// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. #include "GLGizmoAdvancedCut.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "GLGizmosCommon.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_Utils.hpp" #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/Plater.hpp" #include "libslic3r/AppConfig.hpp" #include "../GUI/MsgDialog.hpp" #include "slic3r/GUI/OpenGLManager.hpp" #include #include "FixModelByWin10.hpp" namespace Slic3r { namespace GUI { const double units_in_to_mm = 25.4; const double units_mm_to_in = 1 / units_in_to_mm; const int c_connectors_group_id = 6; const int c_cube_z_move_id = 3; const int c_cube_x_move_id = 4; const int c_plate_move_id = 5; const int c_connectors_start_id = c_connectors_group_id - c_cube_z_move_id; const float UndefFloat = -999.f; // 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 = {0.0f, 0.5f, 0.5f, 1.0f}; static const ColorRGBA SELECTED_PLAG_COLOR = ColorRGBA::GRAY(); static const ColorRGBA SELECTED_DOWEL_COLOR = ColorRGBA::GRAY(); // DARK_GRAY(); static const ColorRGBA CONNECTOR_DEF_COLOR = {1.0f, 1.0f, 1.0f, 0.5f}; static const ColorRGBA CONNECTOR_ERR_COLOR = {1.0f, 0.3f, 0.3f, 0.5f}; static const ColorRGBA HOVERED_ERR_COLOR = {1.0f, 0.3f, 0.3f, 1.0f}; static const ColorRGBA CUT_PLANE_DEF_COLOR = {0.9f, 0.9f, 0.9f, 0.5f}; static const ColorRGBA CUT_PLANE_ERR_COLOR = {1.0f, 0.8f, 0.8f, 0.5f}; static const ColorRGBA UPPER_PART_COLOR = ColorRGBA::CYAN(); static const ColorRGBA LOWER_PART_COLOR = ColorRGBA::MAGENTA(); static const ColorRGBA MODIFIER_COLOR = {0.75f, 0.75f, 0.75f, 0.5f}; static Vec3d rotate_vec3d_around_vec3d_with_rotate_matrix( const Vec3d& rotate_point, const Vec3d& origin_point, const Transform3d& rotate_matrix) { Transform3d translate_to_point = Transform3d::Identity(); translate_to_point.translate(origin_point); Transform3d translate_to_zero = Transform3d::Identity(); translate_to_zero.translate(-origin_point); return (translate_to_point * rotate_matrix * translate_to_zero) * rotate_point; } static inline void rotate_point_2d(double& x, double& y, const double c, const double s) { double xold = x; double yold = y; x = c * xold - s * yold; y = s * xold + c * yold; } static void rotate_x_3d(std::array& verts, float radian_angle) { double c = cos(radian_angle); double s = sin(radian_angle); for (uint32_t i = 0; i < verts.size(); ++i) rotate_point_2d(verts[i](1), verts[i](2), c, s); } static void rotate_y_3d(std::array& verts, float radian_angle) { double c = cos(radian_angle); double s = sin(radian_angle); for (uint32_t i = 0; i < verts.size(); ++i) rotate_point_2d(verts[i](2), verts[i](0), c, s); } static void rotate_z_3d(std::array& verts, float radian_angle) { double c = cos(radian_angle); double s = sin(radian_angle); for (uint32_t i = 0; i < verts.size(); ++i) rotate_point_2d(verts[i](0), verts[i](1), c, s); } namespace { // Rotation taking +Z onto `normal`, which must be unit length. Eigen's // setFromTwoVectors handles the antipodal case (normal == -Z) by picking an // arbitrary perpendicular axis, which is what we want: the plane's X direction // is unconstrained here, exactly as in process_cut_line(). Transform3d rotation_from_plane_normal(const Vec3d &normal) { Eigen::Quaterniond q; Transform3d m = Transform3d::Identity(); m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(Vec3d::UnitZ(), normal).toRotationMatrix(); return m; } } // namespace const double GLGizmoAdvancedCut::Offset = 20.0; const double GLGizmoAdvancedCut::Margin = 20.0; const std::array GLGizmoAdvancedCut::GrabberColor = { 1.0, 1.0, 0.0, 1.0 }; const std::array GLGizmoAdvancedCut::GrabberHoverColor = { 0.7, 0.7, 0.0, 1.0}; GLGizmoAdvancedCut::GLGizmoAdvancedCut(GLCanvas3D& parent, unsigned int sprite_id) : GLGizmoRotate3D(parent, sprite_id, nullptr) , m_movement(0.0) , m_buffered_movement(0.0) , m_keep_upper(true) , m_keep_lower(true) , m_cut_to_parts(false) , m_do_segment(false) , m_segment_smoothing_alpha(0.5) , m_segment_number(5) , m_connector_type(CutConnectorType::Plug) , m_connector_style(size_t(CutConnectorStyle::Prizm)) , m_connector_shape_id(size_t(CutConnectorShape::Circle)) { set_group_id(m_gizmos.size()); m_buffered_rotation.setZero(); } bool GLGizmoAdvancedCut::gizmo_event(SLAGizmoEventType action, const Vec2d &mouse_position, bool shift_down, bool alt_down, bool control_down) { CutConnectors &connectors = m_c->selection_info()->model_object()->cut_connectors; if (shift_down && !m_connectors_editing && (action == SLAGizmoEventType::LeftDown || action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::Dragging)) { process_cut_line(action, mouse_position); return true; } else { // if (!shift_down) discard_cut_line_processing(); } if (action == SLAGizmoEventType::LeftDown) { // Pick-face mode is armed deliberately by the user, so while it is on, a click // means "pick that facet" and nothing else. Grabbers and the cut plane itself are // ignored on purpose: the wanted face is often behind the translucent plane, and // gating on m_hover_id made those faces unclickable. gizmo_event runs before the // manager's grabber handling, so returning true here suppresses the drag. if (m_facet_picker.is_active() && !m_connectors_editing) { m_facet_picker.update(mouse_position, m_c, m_parent.get_selection(), wxGetApp().plater()->get_camera()); if (apply_picked_facet()) m_facet_picker.set_active(false); // one-shot: press the button again to pick another return true; // on a miss, stay armed rather than starting a rotate/pan } if (m_hover_id == c_plate_move_id) { Vec3d pos; Vec3d pos_world; if (unproject_on_cut_plane(mouse_position, pos, pos_world, false)) { m_plane_drag_start = pos_world; } } if (!m_connectors_editing) return false; if (m_hover_id != -1) { start_dragging(); return true; } 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) add_connector(connectors, mouse_position); //m_ldown_mouse_position = mouse_position; } return true; } else if (action == SLAGizmoEventType::LeftUp) { if (m_connectors_editing && m_connector_type == CutConnectorType::Snap && m_add_connector_ok && m_hover_id == -1) { return true;//Snap connector special logic, due to gaps in the middle of the snap, other connectors such as cylinders are solid } if (m_hover_id == -1 && !shift_down && !alt_down) unselect_all_connectors(); is_selection_changed(alt_down, shift_down); return true; } else if (action == SLAGizmoEventType::RightDown) { if (!m_connectors_editing && m_cut_mode == CutMode::cutPlanar) { //&& control_down // Check the internal part raycasters. if (m_part_selection && !m_part_selection->valid()) { process_contours(); } m_part_selection->toggle_selection(mouse_position); check_and_update_connectors_state(); // after a contour is deactivated, its connectors are inside the object return true; } if (m_hover_id < c_connectors_group_id) return false; unselect_all_connectors(); select_connector(m_hover_id - c_connectors_group_id, true); return delete_selected_connectors(); } else if (action == SLAGizmoEventType::RightUp) { // catch right click event return true; } return false; } bool GLGizmoAdvancedCut::on_key(const wxKeyEvent& evt) { bool ctrl_down = evt.GetModifiers() & wxMOD_CONTROL; if (evt.GetKeyCode() == WXK_DELETE) { return delete_selected_connectors(); } else if (ctrl_down && (evt.GetKeyCode() == 'A' || evt.GetKeyCode() == 'a')) { select_all_connectors(); return true; } return false; } std::string GLGizmoAdvancedCut::get_tooltip() const { std::string tooltip; if (m_dragging && (m_hover_id == c_plate_move_id || m_hover_id == c_cube_z_move_id)) { double koef = m_imperial_units ? units_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_cut_to_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_cut_mode == CutMode::cutPlanar && !m_connectors_editing) { // Hybrid hover: GLVolume picking works before PartSelection hides source volumes; // once cut-part preview is active, raycast cut-part meshes instead (overlay is not pickable). const bool hovering_source_volume = m_parent.get_hover_volume_idx_before_gizmo() >= 0; const bool hovering_cut_part = m_part_selection && m_part_selection->valid() && !m_part_selection->is_one_object() && m_part_selection->is_mouse_over_part(m_parent.get_local_mouse_position()); if (hovering_source_volume || hovering_cut_part) return _u8L("Right-click a part to assign it to the other side"); } if (!m_dragging && m_hover_id == c_plate_move_id) return _u8L("Drag to move the cut plane"); if (tooltip.empty() && (m_hover_id == X || m_hover_id == Y || m_hover_id == Z)) { std::string axis = m_hover_id == X ? "X" : m_hover_id == Y ? "Y" : "Z"; return axis + ": " + format(float(Geometry::rad2deg(m_rotate_angle)), 1) + _u8L("°"); } return tooltip; } BoundingBoxf3 GLGizmoAdvancedCut::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 GLGizmoAdvancedCut::transformed_bounding_box(const Vec3d &plane_center, const Transform3d &rotation_m) const { const Selection &selection = m_parent.get_selection(); const auto first_volume = selection.get_first_volume(); BoundingBoxf3 ret; if (!first_volume) { return ret; } 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() * Geometry::translation_transform(instance_offset - plane_center); 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()) { const auto instance_matrix = volume->get_instance_transformation().get_matrix_no_offset(); 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; } bool GLGizmoAdvancedCut::is_looking_forward() const { const Camera &camera = wxGetApp().plater()->get_camera(); const double dot = camera.get_dir_forward().dot(m_plane_normal); return dot < 0.05; } // 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 GLGizmoAdvancedCut::unproject_on_cut_plane(const Vec2d &mouse_pos, Vec3d &pos, Vec3d &pos_world, bool respect_contours) { 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(); Vec3d point; Vec3d direction; Vec3d hit; MeshRaycaster::line_from_mouse_pos_static(mouse_pos, 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; 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&&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; } bool GLGizmoAdvancedCut::apply_picked_facet() { const FacetPicker::Hit &picked = m_facet_picker.hit(); if (!picked.valid() || picked.world_normal.isZero()) return false; const Transform3d m = rotation_from_plane_normal(picked.world_normal); // Plane lands flush with the facet; the user offsets it afterwards with Movement. const Vec3d new_plane_center = picked.world_pos; const auto new_tbb = transformed_bounding_box(new_plane_center, m); // transformed_bounding_box() maps a point to R^-1 * (p_world - plane_center), so new_tbb // is the model's bounding box in the cut plane's own frame and the plane is z = 0 there. // The pick is meaningful when the model actually spans that plane, which is exactly the // test set_center_pos() applies before it will accept the new centre. Asking the same // question here is the point: a guard that disagrees with the function it guards would // either reject a good pick or apply the rotation while the position is silently refused. // // Do not reuse process_cut_line()'s containment check here. It tests // m.inverse() * (plane_center - instance_offset) + new_tbb.center() // which is not the plane's position in this frame. The two terms cancel in X and Y // because an instance origin sits at the model's bounding-box centre in those axes, but // in Z the origin sits at the object's base, so the error is the object's half-height. // process_cut_line() hides that by always cutting through m_bb_center, where the stale Z // still lands inside a tall box. A plane placed flush with a facet - especially on a cut // piece, whose geometry is offset from its instance origin - falls outside and the pick // is silently dropped. const double limit_val = 0.5; if (!(new_tbb.max.z() > -limit_val && new_tbb.min.z() < limit_val)) return false; Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Cut plane from face"); m_transformed_bounding_box = new_tbb; set_center(new_plane_center); m_start_dragging_m = m_rotate_matrix = m; m_plane_normal = m_rotate_matrix * Vec3d::UnitZ(); m_ar_plane_center = m_plane_center; reset_cut_by_contours(); // Keep the Rotation / Movement boxes in step with the new plane. m_movement = 0.0; m_rotation = Geometry::extract_euler_angles(m_rotate_matrix); update_buffer_data(); m_parent.request_extra_frame(); return true; } void GLGizmoAdvancedCut::reset_cut_plane() { m_transformed_bounding_box = transformed_bounding_box(m_bb_center); set_center(m_bb_center); m_start_dragging_m = m_rotate_matrix = Transform3d::Identity(); update_plane_normal(); m_ar_plane_center = m_plane_center; reset_cut_by_contours(); m_parent.request_extra_frame(); const Selection& selection = m_parent.get_selection(); const BoundingBoxf3& box = selection.get_bounding_box(); m_movement = 0.0; m_rotation = Geometry::extract_euler_angles(m_rotate_matrix); update_buffer_data(); } void GLGizmoAdvancedCut::reset_all() { Plater::TakeSnapshot snapshot(wxGetApp().plater(), "reset cut"); if (m_cut_mode == CutMode::cutPlanar) { reset_connectors(); } else if (m_cut_mode == CutMode::cutTongueAndGroove) { m_groove.depth = m_groove.depth_init; m_groove.width = m_groove.width_init; m_groove.flaps_angle = m_groove.flaps_angle_init; m_groove.angle = m_groove.angle_init; m_groove.depth_tolerance = CUT_TOLERANCE; m_groove.width_tolerance = CUT_TOLERANCE; } reset_cut_plane(); m_keep_upper = true; m_keep_lower = true; m_cut_to_parts = false; m_place_on_cut_upper = true; m_place_on_cut_lower = false; m_rotate_upper = false; m_rotate_lower = false; } BoundingBoxf3 GLGizmoAdvancedCut::get_bounding_box() const { BoundingBoxf3 t_aabb; t_aabb.reset(); // rotate aabb auto t_rotate_aabb = GLGizmoRotate3D::get_bounding_box(); if (t_rotate_aabb.defined) { t_aabb.merge(t_rotate_aabb); t_aabb.defined = true; } // end rotate aabb bool is_render_z_grabber = true; // m_hover_id < 0 || m_hover_id == cube_z_move_id; bool is_render_x_grabber = m_cut_mode == CutMode::cutTongueAndGroove; // m_hover_id < 0 || m_hover_id == cube_x_move_id; // m_move_z_grabber aabb if (is_render_z_grabber) { const auto& z_grabber = m_move_z_grabber.get_cube(); if (z_grabber.is_initialized()) { auto z_grabber_aabb = z_grabber.get_bounding_box(); const auto& z_grabber_model_matrix = m_move_z_grabber.m_matrix; z_grabber_aabb = z_grabber_aabb.transformed(z_grabber_model_matrix); z_grabber_aabb.defined = true; t_aabb.merge(z_grabber_aabb); t_aabb.defined = true; } } // end m_move_z_grabber // m_move_x_grabber aabb if (is_render_x_grabber) { const auto& x_grabber = m_move_x_grabber.get_cube(); if (x_grabber.is_initialized()) { auto x_grabber_aabb = x_grabber.get_bounding_box(); const auto& x_grabber_model_matrix = m_move_x_grabber.m_matrix; x_grabber_aabb = x_grabber_aabb.transformed(x_grabber_model_matrix); x_grabber_aabb.defined = true; t_aabb.merge(x_grabber_aabb); t_aabb.defined = true; } } // end m_move_x_grabber // m_plane aabb if (!m_connectors_editing) { if (m_plane.is_initialized()) { auto t_plane_aabb = m_plane.get_bounding_box(); const auto t_plane_model_matrix = Geometry::translation_transform(m_plane_center) * m_rotate_matrix; t_plane_aabb = t_plane_aabb.transformed(t_plane_model_matrix); t_plane_aabb.defined = true; t_aabb.merge(t_plane_aabb); t_aabb.defined = true; } } // end m_plane aabb return t_aabb; } std::string GLGizmoAdvancedCut::get_icon_filename(bool b_dark_mode) const { return b_dark_mode ? "toolbar_cut_dark.svg" : "toolbar_cut.svg"; } bool GLGizmoAdvancedCut::on_init() { if (!GLGizmoRotate3D::on_init()) return false; 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_connector_shortcuts.push_back(std::make_pair(_L("Left click"), _L("Add connector"))); m_connector_shortcuts.push_back(std::make_pair(_L("Right click"), _L("Remove connector"))); m_connector_shortcuts.push_back(std::make_pair(_L("Drag"), _L("Move connector"))); m_connector_shortcuts.push_back(std::make_pair(shift + _L("Left click"), _L("Add connector to selection"))); m_connector_shortcuts.push_back(std::make_pair(alt + _L("Left click"), _L("Remove connector from selection"))); m_connector_shortcuts.push_back(std::make_pair(ctrl + "A", _L("Select all connectors"))); m_cut_plane_shortcuts.push_back(std::make_pair(shift + _L("Left drag"), _L("Plot cut plane"))); m_cut_plane_shortcuts.push_back(std::make_pair(_L("right click"), _L("Assign the part to the other side"))); m_cut_groove_shortcuts.push_back(std::make_pair(shift + _L("Left click"), _L("Plot cut plane"))); init_connector_shapes(); return true; } std::string GLGizmoAdvancedCut::on_get_name() const { if (!on_is_activable() && m_state == EState::Off) { return _u8L("Cut") + ":\n" + _u8L("Please select single object."); } else { return _u8L("Cut"); } } void GLGizmoAdvancedCut::apply_color_clip_plane_colors() { m_parent.set_color_clip_plane_colors({UPPER_PART_COLOR, LOWER_PART_COLOR}); } void GLGizmoAdvancedCut::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_rotate_matrix, groove_depth, groove_width, groove_flaps_angle, groove_angle, groove_depth_tolerance, groove_width_tolerance); m_start_dragging_m = m_rotate_matrix; m_transformed_bounding_box = transformed_bounding_box(m_ar_plane_center, m_rotate_matrix); set_center_pos(m_ar_plane_center); m_rotation = Geometry::extract_euler_angles(m_rotate_matrix); m_movement = 0; if (m_cut_mode != (CutMode) mode) switch_to_mode((CutMode) mode); else if (m_cut_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(); } else if (m_cut_mode == CutMode::cutPlanar) { reset_cut_by_contours(); } m_parent.request_extra_frame(); update_buffer_data(); } void GLGizmoAdvancedCut::on_save(cereal::BinaryOutputArchive &ar) const { ar(m_keep_upper, m_keep_lower, m_rotate_lower, m_rotate_upper, m_hide_cut_plane, (size_t) m_cut_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); } void GLGizmoAdvancedCut::data_changed(bool is_serializing) { reset_rotation(); if (m_hover_id < 0) { // BBL update_bb(); if (auto oc = m_c->object_clipper()) { oc->set_behaviour(m_connectors_editing, m_connectors_editing, double(m_contour_width)); } } } void GLGizmoAdvancedCut::on_set_state() { GLGizmoRotate3D::on_set_state(); // Reset m_cut_z on gizmo activation if (get_state() == On) { m_parent.set_use_color_clip_plane(true); const Selection &selection = m_parent.get_selection(); if (selection.is_empty()) {//check selection again close(); return; } m_hover_id = -1; m_connectors_editing = false; m_facet_picker.set_active(false); update_bb(); reset_cut_plane();//according to boundingbox m_connectors_editing = !m_selected.empty(); m_transformed_bounding_box = transformed_bounding_box(m_plane_center, m_rotate_matrix); // initiate archived values m_ar_plane_center = m_plane_center; m_start_dragging_m = m_rotate_matrix; m_parent.request_extra_frame(); } else if (get_state() == Off) { toggle_model_objects_visibility(true); m_parent.set_use_dovetail_clip(false); if (auto oc = m_c->object_clipper()) { oc->set_behaviour(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 && m_part_selection->valid()) { wxGetApp().CallAfter([this]() { m_part_selection.reset(new PartSelection()); }); } } } void GLGizmoAdvancedCut::close() {//close gizmo == open it again auto &mng = m_parent.get_gizmos_manager(); if (mng.get_current_type() == GLGizmosManager::Cut) mng.open_gizmo(GLGizmosManager::Cut); } bool GLGizmoAdvancedCut::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(); } CommonGizmosDataID GLGizmoAdvancedCut::on_get_requirements() const { return CommonGizmosDataID(int(CommonGizmosDataID::SelectionInfo) | int(CommonGizmosDataID::InstancesHider) | int(CommonGizmosDataID::ObjectClipper)); } void GLGizmoAdvancedCut::on_start_dragging() { if (m_connectors_editing && m_hover_id >= c_connectors_group_id) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Move connector"); return; } else if (m_hover_id >= 0 && m_hover_id <= 2) { for (auto& gizmo : m_gizmos) { if (m_hover_id == gizmo.get_group_id()) { gizmo.start_dragging(); break; } } m_rotate_angle = 0; } else if (m_hover_id >= c_cube_z_move_id && c_connectors_group_id) { const Selection & selection = m_parent.get_selection(); const BoundingBoxf3 &box = selection.get_bounding_box(); m_start_movement = 0; m_start_height = m_plane_center.z(); m_plane_center_drag_start = m_plane_center; if (m_hover_id == c_cube_z_move_id) { m_drag_pos_start = m_move_z_grabber.center; } else if (m_hover_id == c_cube_x_move_id) { m_drag_pos_start = m_move_x_grabber.center; } else if (m_hover_id == c_plate_move_id) { m_drag_pos_start = m_plane_drag_start; } } m_start_dragging_m = m_rotate_matrix; } void GLGizmoAdvancedCut::on_stop_dragging() { if (m_is_dragging ==false) { return; } m_is_dragging = false; if (m_hover_id >= 0 && m_hover_id <= 2) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Rotate cut plane"); m_start_dragging_m = m_rotate_matrix; // for takeshot for (auto& gizmo : m_gizmos) { if (m_hover_id == gizmo.get_group_id()) { gizmo.stop_dragging(); break; } } } else if (m_hover_id == c_cube_z_move_id || m_hover_id == c_cube_x_move_id || m_hover_id == c_plate_move_id) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Move cut plane"); // todo m_ar_plane_center = m_plane_center; } m_plane_center_drag_start = m_plane_center; if (!m_connectors_editing) { reset_cut_by_contours(); } m_movement = 0.0; m_rotation = Geometry::extract_euler_angles(m_rotate_matrix); update_buffer_data(); } void GLGizmoAdvancedCut::update_plate_center(Axis axis_type, double projection, bool is_abs_move) { const Vec3d shift = m_rotate_matrix * ((axis_type == Axis::Z ? Vec3d::UnitZ() : axis_type == Axis::Y ? Vec3d::UnitY() : Vec3d::UnitX()) * projection); if (shift != Vec3d::Zero()) { // update m_plane_center if (is_abs_move) { this->set_center(m_plane_center_drag_start + shift, true); } else { this->set_center(m_plane_center + shift, true); } } } void GLGizmoAdvancedCut::update_plate_normal_boundingbox_clipper(const Transform3d &rotation_tmp) { const bool update_tbb = !m_rotate_matrix.rotation().isApprox(rotation_tmp.rotation()); if (update_tbb) { m_rotate_matrix = rotation_tmp; m_transformed_bounding_box = transformed_bounding_box(m_plane_center, m_rotate_matrix); update_clipper(); } } void GLGizmoAdvancedCut::on_update(const UpdateData& data) { // 1. GHOST LOGIC: Update position BEFORE the gatekeeper if (m_connectors_editing) { Vec3d pos; Vec3d pos_world; if (unproject_on_cut_plane(data.mouse_pos.cast(), pos, pos_world)) { m_localized_cut_pos = pos_world; } } if (m_hover_id < 0) return; m_is_dragging = true; if (m_hover_id <= 2) { // drag rotate GLGizmoRotate3D::on_update(data); Vec3d rotation{ 0.0f, 0.0f, 0.0f }; for (int i = 0; i < 3; i++) { if (m_gizmos[i].is_dragging()) { rotation(i) = m_gizmos[i].get_angle(); if (rotation(i) < 0) rotation(i) = 2 * PI + rotation(i); m_rotate_angle = rotation(i); break; } } const Transform3d rotation_tmp = m_start_dragging_m * Geometry::rotation_transform(rotation); // deal rotate if (!is_approx(rotation, Vec3d(0, 0, 0))) { update_plate_normal_boundingbox_clipper(rotation_tmp); } } // move plane else if (m_hover_id == c_cube_z_move_id || m_hover_id == c_plate_move_id) { double move = calc_projection(m_drag_pos_start, data.mouse_ray, m_plane_normal); m_buffered_movement = m_movement = m_start_movement + move; update_plate_center(Axis::Z, move, true); } // move x else if (m_hover_id == c_cube_x_move_id && m_cut_mode == CutMode::cutTongueAndGroove) { double move = calc_projection(m_drag_pos_start, data.mouse_ray, m_plane_x_direction); m_buffered_movement = m_movement = m_start_movement + move; update_plate_center(Axis::X, move, true); } // drag existing connectors else if (m_connectors_editing && m_hover_id >= c_connectors_group_id) { 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 - c_connectors_group_id].pos = pos; } } check_and_update_connectors_state(); } void GLGizmoAdvancedCut::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(); update_dovetail_preview_clip(); // Show placed connectors if (m_connectors_editing) { render_connectors(); } if (m_part_selection) { if (!m_connectors_editing) { if (m_is_dragging == false) { m_part_selection->part_render(nullptr, nullptr); } } else { m_part_selection->part_render(&m_plane_center, &m_plane_normal); } } if (!m_connectors_editing) { render_cut_plane_and_grabbers(); } if (m_facet_picker.is_active() && !m_connectors_editing) m_facet_picker.render(wxGetApp().plater()->get_camera()); // render_clipper_cut for get the cut plane result render_clipper_cut(); // render a cut line on screen by shift key and mouse move render_cut_line(); // THE VISIBILITY FIX: Draw the ghost absolute last so it paints ON TOP of everything if (m_connectors_editing && m_hover_id < c_connectors_group_id) { render_localized_cut_shadow(); } } bool GLGizmoAdvancedCut::on_mouse(const wxMouseEvent &mouse_event) { if (m_facet_picker.is_active() && !m_connectors_editing) { if (mouse_event.Moving() || mouse_event.Dragging()) { m_facet_picker.update(Vec2d(mouse_event.GetX(), mouse_event.GetY()), m_c, m_parent.get_selection(), wxGetApp().plater()->get_camera()); m_parent.request_extra_frame(); } else if (mouse_event.Leaving()) { m_facet_picker.reset(); } } // If we are in connector mode and the mouse is hovering... if (m_connectors_editing && mouse_event.Moving()) { Vec3d pos; Vec3d pos_world; // Grab the raw X/Y from the mouse and unproject it to the 3D cut plane if (unproject_on_cut_plane(Vec2d(mouse_event.GetX(), mouse_event.GetY()), pos, pos_world)) { // --- THE MAGNET MATH (HOVER PREVIEW) --- if (m_auto_center_connector && m_c && m_c->object_clipper()) { int contour_idx = m_c->object_clipper()->is_projection_inside_cut(pos_world); if (contour_idx >= 0) { // THE HEURISTIC: Is it a simple shape or a complex split? if (m_c->object_clipper()->get_number_of_contours() == 1) { m_localized_cut_pos = m_plane_center; // Mathematical perfection! } else { // Complex shape approximation! std::vector contour_centers = m_c->object_clipper()->point_per_contour(); if (contour_idx < contour_centers.size()) { Vec3d center_3d = contour_centers[contour_idx]; double dist = (center_3d - m_plane_center).dot(m_plane_normal); m_localized_cut_pos = center_3d - dist * m_plane_normal; } else { m_localized_cut_pos = pos_world; } } } else { m_localized_cut_pos = pos_world; } } else { m_localized_cut_pos = pos_world; } // ---------------------------------------- // Force the 3D canvas to update the frame immediately wxGetApp().plater()->get_current_canvas3D()->request_extra_frame(); } } // Pass the event back up the chain! return GLGizmoRotate3D::on_mouse(mouse_event); } void GLGizmoAdvancedCut::render_localized_cut_shadow() { ::glEnable(GL_BLEND); ::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); ::glEnable(GL_CULL_FACE); ::glDisable(GL_DEPTH_TEST); ::glDepthMask(GL_FALSE); const Camera& camera = wxGetApp().plater()->get_camera(); const auto& view_matrix = camera.get_view_matrix(); const auto& projection_matrix = camera.get_projection_matrix(); ColorRGBA shadow_color(0.38f, 0.41f, 0.57f, 0.65f); // Calculate transformations Transform3d translate_tf = Transform3d::Identity(); translate_tf.translate(m_localized_cut_pos); Transform3d rotate_tf = Transform3d::Identity(); rotate_tf.rotate(Eigen::Quaterniond::FromTwoVectors(Vec3d::UnitZ(), m_plane_normal)); Transform3d scale_tf = Transform3d::Identity(); scale_tf.scale(Vec3f(m_connector_size / 2.0, m_connector_size / 2.0, m_connector_depth_ratio).cast()); const Transform3d view_model_matrix = view_matrix * translate_tf * rotate_tf * scale_tf; if (m_shapes.empty()) { init_connector_shapes(); } if (!m_shapes.empty()) { GLModel* shape_to_render = nullptr; // Find the correct shape for (auto& pair : m_shapes) { if (pair.first.type == m_connector_type && (int)pair.first.shape == (int)m_connector_shape_id) { shape_to_render = &pair.second; break; } } if (!shape_to_render) { shape_to_render = &m_shapes.begin()->second; } // Render it render_glmodel(*shape_to_render, shadow_color.get_data(), view_model_matrix, projection_matrix); } ::glEnable(GL_DEPTH_TEST); ::glDepthMask(GL_TRUE); ::glEnable(GL_CULL_FACE); ::glDisable(GL_BLEND); } void GLGizmoAdvancedCut::on_render_for_picking() { const Camera& camera = wxGetApp().plater()->get_picking_camera(); const auto& view_matrix = camera.get_view_matrix(); const auto& projection_matrix = camera.get_projection_matrix(); glsafe(::glDisable(GL_DEPTH_TEST)); if (!m_connectors_editing) { std::array color; // pick plane { color = picking_color_component(2); render_glmodel(m_plane, color, view_matrix * Geometry::translation_transform(m_plane_center) * m_rotate_matrix, projection_matrix, true); } // pick Rotate GLGizmoRotate3D::on_render_for_picking(); BoundingBoxf3 box = m_parent.get_selection().get_bounding_box(); // pick grabber { const auto& flat_shader = wxGetApp().get_shader("flat"); wxGetApp().bind_shader(flat_shader); color = picking_color_component(0); m_move_z_grabber.color[0] = color[0]; m_move_z_grabber.color[1] = color[1]; m_move_z_grabber.color[2] = color[2]; m_move_z_grabber.color[3] = color[3]; m_move_z_grabber.render_for_picking(); if (m_cut_mode == CutMode::cutTongueAndGroove) { color = picking_color_component(1); m_move_x_grabber.color[0] = color[0]; m_move_x_grabber.color[1] = color[1]; m_move_x_grabber.color[2] = color[2]; m_move_x_grabber.color[3] = color[3]; m_move_x_grabber.render_for_picking(); } wxGetApp().unbind_shader(); } } else { auto inst_id = m_c->selection_info()->get_active_instance(); if (inst_id < 0) return; const ModelObject * mo = m_c->selection_info()->model_object(); 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 CutConnectors &connectors = mo->cut_connectors; const bool looking_forward = is_looking_forward(); for (int i = 0; i < connectors.size(); ++i) { CutConnector connector = connectors[i]; Vec3d pos = connector.pos + instance_offset + sla_shift * Vec3d::UnitZ(); float height = connector.height; deal_connector_pos_by_type(pos, height, connector.attribs.type, connector.attribs.style, looking_forward, m_connectors_editing, m_clp_normal); Transform3d translate_tf = Transform3d::Identity(); translate_tf.translate(pos); Transform3d scale_tf = Transform3d::Identity(); scale_tf.scale(Vec3f(connector.radius, connector.radius, height).cast()); const Transform3d view_model_matrix = view_matrix * translate_tf * m_rotate_matrix * scale_tf; std::array color = picking_color_component(i + c_connectors_start_id); render_glmodel(m_shapes[connectors[i].attribs], color, view_model_matrix, projection_matrix, true); } } } void GLGizmoAdvancedCut::on_render_input_window(float x, float y, float bottom_limit) { GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 0.0f, 0.0f); ImGuiWrapper::push_toolbar_style(m_parent.get_scale()); GizmoImguiBegin(on_get_name(), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); m_control_width = m_imgui->get_font_size() * 9.f; m_editing_window_width = 1.55 * m_control_width; if (m_connectors_editing) { init_connectors_input_window_data(); render_connectors_input_window(x, y, bottom_limit); } else render_cut_plane_input_window(x, y, bottom_limit); render_input_window_warning(); GizmoImguiEnd(); ImGuiWrapper::pop_toolbar_style(); } void GLGizmoAdvancedCut::show_tooltip_information(float x, float y) { float caption_max = 0.f; if (m_connectors_editing) { for (const auto &short_cut : m_connector_shortcuts) { caption_max = std::max(caption_max, m_imgui->calc_text_size(short_cut.first).x); } } else if (m_cut_mode == CutMode::cutPlanar) { for (const auto &short_cut : m_cut_plane_shortcuts) { caption_max = std::max(caption_max, m_imgui->calc_text_size(short_cut.first).x); } } else if (m_cut_mode == CutMode::cutTongueAndGroove) { for (const auto &short_cut : m_cut_groove_shortcuts) { caption_max = std::max(caption_max, m_imgui->calc_text_size(short_cut.first).x); } } ImTextureID normal_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP); ImTextureID hover_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP_HOVER); caption_max += m_imgui->calc_text_size(": ").x + 35.f; float font_size = ImGui::GetFontSize(); ImVec2 button_size = ImVec2(font_size * 1.8, font_size * 1.3); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {0, ImGui::GetStyle().FramePadding.y}); ImGui::ImageButton3(normal_id, hover_id, button_size); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip2(ImVec2(x, y)); auto draw_text_with_caption = [this, &caption_max](const wxString &caption, const wxString &text) { m_imgui->text_colored(ImGuiWrapper::COL_ACTIVE, caption); ImGui::SameLine(caption_max); m_imgui->text_colored(ImGuiWrapper::COL_WINDOW_BG, text); }; if (m_connectors_editing) { for (const auto &short_cut : m_connector_shortcuts) draw_text_with_caption(short_cut.first + ": ", short_cut.second); } else if (m_cut_mode == CutMode::cutPlanar) { for (const auto &short_cut : m_cut_plane_shortcuts) draw_text_with_caption(short_cut.first + ": ", short_cut.second); } else if (m_cut_mode == CutMode::cutTongueAndGroove) { for (const auto &short_cut : m_cut_groove_shortcuts) draw_text_with_caption(short_cut.first + ": ", short_cut.second); } ImGui::EndTooltip(); } ImGui::PopStyleVar(2); } 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)); } } static void check_objects_after_cut(const ModelObjectPtrs &objects) { std::vector err_objects_names; std::vector err_objects_idxs; int obj_idx{0}; for (const ModelObject *object : objects) { std::vector connectors_names; connectors_names.reserve(object->volumes.size()); for (const ModelVolume *vol : object->volumes) if (vol->cut_info.is_connector) connectors_names.push_back(vol->name); const size_t connectors_count = connectors_names.size(); sort_remove_duplicates(connectors_names); if (connectors_count != connectors_names.size()) err_objects_names.push_back(object->name); // check manifol/repairs auto stats = object->get_object_stl_stats(); if (!stats.manifold() || stats.has_open_edges() || stats.repaired()) err_objects_idxs.push_back(obj_idx); obj_idx++; } auto plater = wxGetApp().plater(); if (!err_objects_names.empty()) { wxString names = from_u8(err_objects_names[0]); for (size_t i = 1; i < err_objects_names.size(); i++) names += ", " + from_u8(err_objects_names[i]); WarningDialog(plater, format_wxstr("Objects(%1%) have duplicated connectors. " "Some connectors may be missing in slicing result.\n" "Please report to BambuSudio team in which scenario this issue happened.\n" "Thank you.", names)) .ShowModal(); } } 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 GLGizmoAdvancedCut::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, "GLGizmoAdvancedCut: Invalid object selection"); Plater * plater = wxGetApp().plater(); ModelObject *mo = plater->model().objects[object_idx]; if (!mo) return; // Cutting rebuilds/splits the mesh; painted color, supports, seam and // fuzzy-skin may not transfer cleanly. Warn the user when any exist. if (mo->is_mm_painted() || mo->is_fuzzy_skin_painted() || mo->is_fdm_support_painted() || mo->is_seam_painted()) { if (!wxGetApp().confirm_mesh_paint_warning()) return; } // deactivate CutGizmo and than perform a cut m_parent.reset_all_gizmos(); // m_cut_z is the distance from the bed. Subtract possible SLA elevation. // const GLVolume* first_glvolume = selection.get_volume(*selection.get_volume_idxs().begin()); // Shared with the worker below. Declared outside the snapshot scope, together with a guard // that rolls the model back once the snapshot is closed: baking the connectors into the model // and stamping the cut id already modified it, so a cancel is not just "don't commit". // // Roll back to an absolute point in the undo stack rather than one step back: the main thread // pumps the event loop while the worker runs, so a CallAfter or a background notification can // push snapshots of its own in between, and undo() would then land on the wrong state and // leave the baked connectors and the stamped cut_id behind. const size_t pre_cut_snapshot_time = plater->get_active_snapshot_time(); std::atomic canceled(false); ScopeGuard cut_rollback([plater, pre_cut_snapshot_time, &canceled]() { if (!canceled) return; if (plater->get_active_snapshot_time() != pre_cut_snapshot_time + 1) BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": snapshots were taken while the cut was running, " << "rolling back to " << pre_cut_snapshot_time; plater->undo_redo_to(pre_cut_snapshot_time); }); // perform cut { Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Cut by Plane"); // This shall delete the part selection class and deallocate the memory. ScopeGuard part_selection_killer([this]() { m_part_selection.reset(new PartSelection()); }); const bool cut_with_groove = m_cut_mode == CutMode::cutTongueAndGroove; bool cut_by_contour = !cut_with_groove && m_part_selection->valid() && m_part_selection->has_modified_cut_parts(); 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); 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_cut_to_parts, ModelObjectCutAttribute::CutToParts) | 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); cut.set_offset_for_two_part = true; // reset_all_gizmos() above posted a CallAfter() that replaces m_part_selection, which owns // cut_mo when cutting by contour. The event loop runs while we wait for the worker below, so // that replacement can happen mid-cut: read everything still needed from cut_mo here, on the // main thread, and never touch it again afterwards. The Cut already holds its own deep copy. const std::vector cut_parts = cut_by_contour ? m_part_selection->get_cut_parts() : std::vector(); // Snapshot the groove parameters too, for the same reason: the worker must not read gizmo // state that the pumped event loop could change under it. const Groove groove = m_groove; const Transform3d rotate_matrix = m_rotate_matrix; // save cut_id to post update synchronization const CutObjectBase cut_id = cut_mo->cut_id; const ModelObjectPtrs *new_objects_ptr = nullptr; { // Structured like fix_model_by_win10_sdk_gui(): the cut runs in a worker while the main // thread keeps pumping the event loop, so the window stays responsive and the Cancel // click is never missed. Scoped so this dialog is gone before the repair dialog below // opens its own app-modal one. std::mutex mutex; std::condition_variable condition; struct Progress { std::string message; int percent = 0; bool updated = false; } progress; std::atomic finished(false); std::string cut_error; cut.set_progress( [&mutex, &condition, &progress](int percent, const char *message) { std::lock_guard lock(mutex); // Geometry steps always pass "Cutting model object"; paint steps pass the // Restoring labels. Null only bumps the percent and leaves the label alone. if (message) progress.message = message; // Each nesting level maps onto its own slice of the range, but keep the bar // monotonic even if a level reports the start of its slice late. if (percent > progress.percent) progress.percent = percent; progress.updated = true; condition.notify_all(); }, [&canceled]() { return canceled.load(); }); ProgressDialog progress_dlg(_L("Cutting model object"), "", 100, find_toplevel_parent(plater), wxPD_AUTO_HIDE | wxPD_APP_MODAL | wxPD_CAN_ABORT, true); progress_dlg.EnableYield(true); boost::thread worker_thread([&]() { try { new_objects_ptr = &(cut_by_contour ? cut.perform_by_contour(cut_parts, dowels_count) : cut_with_groove ? cut.perform_with_groove(groove, rotate_matrix) : cut.perform_with_plane()); } catch (std::exception &ex) { cut_error = ex.what(); } catch (...) { // Anything escaping a boost::thread function terminates the process, so the // catch-all has to stay even though nothing here is expected to throw one. cut_error = "unknown exception"; } { // Set under the lock: the waiter evaluates its predicate while holding it, so // notifying from the outside can slip through the gap and cost a 50ms timeout. std::lock_guard lock(mutex); finished = true; } condition.notify_all(); }); wxString last_label; std::unique_lock lock(mutex); while (!finished) { const std::string message = progress.message; const int percent = progress.percent; progress.updated = false; lock.unlock(); // Report one percent less so wxPD_AUTO_HIDE does not close the dialog early. const int dialog_percent = percent > 0 ? percent - 1 : 0; const wxString label = message.empty() ? _L("Cutting model object") : _L(message); if (progress_dlg.WasCancelled() || !progress_dlg.Update(dialog_percent, label)) { canceled = true; } else if (label != last_label) { // Only when the stage label changes: fitting on every tick makes the dialog // jitter as the width is recomputed 20 times a second. last_label = label; progress_dlg.Fit(); } lock.lock(); condition.wait_for(lock, std::chrono::milliseconds(50), [&progress, &finished]() { return progress.updated || finished.load(); }); } lock.unlock(); worker_thread.join(); if (!cut_error.empty()) { // Treat a failure like a cancellation: nothing gets written back and the model is // rolled back, rather than letting the exception cross the thread boundary. BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": the cut failed: " << cut_error; canceled = true; } if (cut.was_canceled() || new_objects_ptr == nullptr) canceled = true; // The callbacks reference locals of this scope, and cut outlives it because it owns // the resulting objects. cut.set_progress(nullptr, nullptr); } if (canceled) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": the cut was canceled, rolling back"; // cut_rollback below undoes the snapshot once this scope closes. return; } const ModelObjectPtrs &new_objects = *new_objects_ptr; check_objects_after_cut(new_objects);// Fix for #11487 - Cut Connectors Broken when assigning part to other side // fix_non_manifold_edges #ifdef HAS_WIN10SDK if (is_windows10()) { bool is_showed_dialog = false; bool user_fix_model = false; bool all_break = false; Plater::TakeSnapshot snapshot(plater, "RepairingModelObjectInCut"); for (size_t i = 0; i < new_objects.size(); i++) { for (size_t j = 0; j < new_objects[i]->volumes.size(); j++) { if (its_num_open_edges(new_objects[i]->volumes[j]->mesh().its) > 0) { if (!is_showed_dialog) { is_showed_dialog = true; MessageDialog dlg(nullptr, _L("Open edges may be caused by the cut tool, do you want to fix it now?"), "", wxYES | wxNO); int ret = dlg.ShowModal(); if (ret == wxID_YES) { user_fix_model = true; } } if (!user_fix_model) { break; } // model_name std::vector succes_models; // model_name failing reason std::vector> failed_models; auto plater = wxGetApp().plater(); auto fix_and_update_progress = [this, plater](ModelObject *model_object, const int vol_idx, const string &model_name, ProgressDialog &progress_dlg, std::vector &succes_models, std::vector> &failed_models) { wxString msg = _L("Repairing model object"); msg += ": " + from_u8(model_name) + "\n"; std::string res; if (!fix_model_by_win10_sdk_gui(*model_object, vol_idx, progress_dlg, msg, res)) return false; return true; }; ProgressDialog progress_dlg(_L("Repairing model object"), "", 100, find_toplevel_parent(plater), wxPD_AUTO_HIDE | wxPD_APP_MODAL | wxPD_CAN_ABORT, true); auto model_name = new_objects[i]->name; if (!fix_and_update_progress(new_objects[i], j, model_name, progress_dlg, succes_models, failed_models)) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "run fix_and_update_progress error"; plater->take_snapshot("RepairingModelObjectInCut"); all_break = true; break; }; }; } if (all_break) { break; } } } #endif // set offset for new_objects // 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); } } bool GLGizmoAdvancedCut::can_perform_cut() const { if (!m_invalid_connectors_idxs.empty() || (!m_keep_upper && !m_keep_lower) || m_connectors_editing) return false; if (m_cut_mode == CutMode::cutTongueAndGroove) return has_valid_groove(); if (m_part_selection && m_part_selection->valid()) return !m_part_selection->is_one_object(); return true; } void GLGizmoAdvancedCut::apply_connectors_in_model(ModelObject *mo, int &dowels_count) { if (m_cut_mode == CutMode::cutTongueAndGroove) return; { clear_selection(); for (CutConnector &connector : mo->cut_connectors) { connector.rotation_m = m_rotate_matrix; // m_rotation_m if (connector.attribs.type == CutConnectorType::Dowel) { if (connector.attribs.style == CutConnectorStyle::Prizm) connector.height *= 2; dowels_count++; } else { // calculate shift of the connector center regarding to the position on the cut plane connector.pos += m_plane_normal * 0.5 * double(connector.height); } } mo->apply_cut_connectors(_u8L("Connector")); } } bool GLGizmoAdvancedCut::is_selection_changed(bool alt_down, bool shift_down) { if (m_hover_id >= c_connectors_group_id) { if (alt_down) select_connector(m_hover_id - c_connectors_group_id, false); else { if (!shift_down) unselect_all_connectors(); select_connector(m_hover_id - c_connectors_group_id, true); } return true; } return false; } void GLGizmoAdvancedCut::select_connector(int idx, bool select) { m_selected[idx] = select; if (select) ++m_selected_count; else --m_selected_count; } double GLGizmoAdvancedCut::calc_projection(const Vec3d &drag_pos, const Linef3 &mouse_ray, const Vec3d &project_dir) const { Vec3d mouse_dir = mouse_ray.unit_vector(); Vec3d inters = mouse_ray.a + (drag_pos - mouse_ray.a).dot(mouse_dir) / mouse_dir.squaredNorm() * mouse_dir; Vec3d inters_vec = inters - drag_pos; return inters_vec.dot(project_dir); } Vec3d GLGizmoAdvancedCut::get_plane_normal() const { return m_plane_normal; } Vec3d GLGizmoAdvancedCut::get_plane_center() const { return m_plane_center; } void GLGizmoAdvancedCut::reset_rotation() { for (int i = 0; i < 3; i++) { m_gizmos[i].set_angle(0.); } } void GLGizmoAdvancedCut::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; } } } void GLGizmoAdvancedCut::update_plane_normal() { // update cut_normal Vec3d normal = m_rotate_matrix * Vec3d::UnitZ(); m_plane_normal = normal; // core m_plane_x_direction = m_rotate_matrix * Vec3d::UnitX(); // core m_clp_normal = normal; } void GLGizmoAdvancedCut::update_clipper() { update_plane_normal(); auto normal = m_plane_normal; // calculate normal and offset for clipping plane Vec3d beg = m_bb_center; beg[Z] -= m_radius; rotate_vec3d_around_plane_center(beg, m_rotate_matrix, m_plane_center); 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_rotate_matrix * (-1. * Vec3d::UnitZ()); normal.normalize(); beg = m_bb_center; beg[Z] += m_radius; rotate_vec3d_around_plane_center(beg, m_rotate_matrix, m_plane_center); m_clp_normal = normal; offset = normal.dot(m_plane_center); dist = normal.dot(beg); } if (m_c->object_clipper()) { m_c->object_clipper()->set_range_and_pos(normal, offset, dist); put_connectors_on_cut_plane(normal, offset); } } void GLGizmoAdvancedCut::update_dovetail_preview_clip() { if (m_cut_mode != CutMode::cutTongueAndGroove || m_connectors_editing || m_dragging || m_groove_editing || !has_valid_groove_shape()) { m_parent.set_use_dovetail_clip(false); return; } const Transform3d groove_to_world = Geometry::translation_transform(m_plane_center) * m_rotate_matrix; m_parent.set_dovetail_clip( groove_to_world.inverse(), Vec4f(m_groove.depth, m_groove.width, m_groove.flaps_angle, m_groove.angle), Vec2f(m_groove.depth_tolerance, m_groove.width_tolerance)); m_parent.set_use_dovetail_clip(true); } void GLGizmoAdvancedCut::render_cut_plane_and_grabbers() { // plane points is in object coordinate const auto& ogl_manager = wxGetApp().get_opengl_manager(); if (!ogl_manager) { return; } // Draw cut plane above PartPlate / scene geometry (same idea as grabbers below). glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); glsafe(::glEnable(GL_DEPTH_TEST)); glsafe(::glDisable(GL_CULL_FACE)); glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); bool is_valid = m_cut_mode == CutMode::cutTongueAndGroove ? has_valid_groove() : can_perform_cut(); ColorRGBA cp_clr = is_valid ? CUT_PLANE_DEF_COLOR : CUT_PLANE_ERR_COLOR; if (m_cut_mode == CutMode::cutTongueAndGroove) { cp_clr.a(cp_clr.a() - 0.1f); } const Camera& camera = wxGetApp().plater()->get_camera(); const auto& view_matrix = camera.get_view_matrix(); const auto& projection_matrix = camera.get_projection_matrix(); render_glmodel(m_plane, cp_clr.get_data(), view_matrix * Geometry::translation_transform(m_plane_center) * m_rotate_matrix, projection_matrix); glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); glsafe(::glEnable(GL_CULL_FACE)); glsafe(::glDisable(GL_BLEND)); // Draw the grabber and the connecting line float radius = Offset; modify_radius(radius); radius *= 0.5f; m_move_z_grabber.center = m_plane_center + m_plane_normal * radius; bool is_render_z_grabber = true; // m_hover_id < 0 || m_hover_id == cube_z_move_id; bool is_render_x_grabber = m_cut_mode == CutMode::cutTongueAndGroove; // m_hover_id < 0 || m_hover_id == cube_x_move_id; glsafe(::glDisable(GL_DEPTH_TEST)); if (!m_grabber_model.is_initialized()) { GLModel::Geometry init_data; init_data.format = { GLModel::PrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; init_data.reserve_vertices(2); init_data.reserve_indices(2); // vertices init_data.add_vertex(Vec3f(-0.5f, 0.0f, 0.0f)); init_data.add_vertex(Vec3f(0.5f, 0.0f, 0.0f)); // indices init_data.add_line(0, 1); m_grabber_model.init_from(std::move(init_data)); } m_grabber_model.set_color({ 1.0f, 1.0f, 0.0f, 1.0f }); auto calculate_model_matrix = [](const Vec3d& source, const Vec3d& target, Transform3d& model_matrix)->void { Vec3d the_vector = target - source; const auto scale = the_vector.stableNorm(); const Vec3d center = source + 0.5f * scale * the_vector.normalized(); Vec3d rotation_axis; double rotation_angle; Matrix3d rotation_matrix; Geometry::rotation_from_two_vectors(Vec3d(1.0f, 0.0f, 0.0f), the_vector, rotation_axis, rotation_angle, &rotation_matrix); Matrix4d final_rotation_matrix{ Matrix4d::Identity() }; for (int iColumn = 0; iColumn < 3; ++iColumn) { for (int jRow = 0; jRow < 3; ++jRow) { final_rotation_matrix.data()[4 * iColumn + jRow] = rotation_matrix.data()[3 * iColumn + jRow]; } } model_matrix = Geometry::translation_transform(center.cast()).matrix() * final_rotation_matrix * Geometry::scale_transform({ scale, scale, scale }).matrix(); }; const float fullsize = get_grabber_size(); const auto& p_flat_shader = wxGetApp().get_shader("flat"); if (p_flat_shader && (is_render_z_grabber || is_render_x_grabber)) { wxGetApp().bind_shader(p_flat_shader); const auto& view_matrix = camera.get_view_matrix(); const auto& proj_matrix = camera.get_projection_matrix(); p_flat_shader->set_uniform("projection_matrix", proj_matrix); ogl_manager->set_line_width(m_hover_id != -1 ? 2.0f : 1.5f); // to do: remove deprecated api: glLineStipple #ifdef __APPLE__ const auto &gl_info = ogl_manager->get_gl_info(); const auto formated_gl_version = gl_info.get_formated_gl_version(); if (formated_gl_version < 30) #endif { glLineStipple(1, 0x0FFF); glEnable(GL_LINE_STIPPLE); } if (is_render_z_grabber) { Transform3d model_matrix{ Transform3d::Identity() }; calculate_model_matrix(m_plane_center, m_move_z_grabber.center, model_matrix); p_flat_shader->set_uniform("view_model_matrix", view_matrix * model_matrix); m_grabber_model.render_geometry(); } m_move_x_grabber.center = m_plane_center + m_plane_x_direction * radius; if (is_render_x_grabber) { Transform3d model_matrix{ Transform3d::Identity() }; calculate_model_matrix(m_plane_center, m_move_x_grabber.center, model_matrix); p_flat_shader->set_uniform("view_model_matrix", view_matrix * model_matrix); m_grabber_model.render_geometry(); } #ifdef __APPLE__ if (formated_gl_version < 30) #endif { glDisable(GL_LINE_STIPPLE); } } // BBS set to fixed size grabber // float fullsize = 2 * (dragging ? get_dragging_half_size(size) : get_half_size(size)); GLModel &cube_z = m_move_z_grabber.get_cube(); const auto get_render_color = [this](unsigned int id)->std::array { bool hover = (m_hover_id == id); std::array render_color; if (hover) { render_color = GrabberHoverColor; } else render_color = GrabberColor; return render_color; }; if (is_render_z_grabber) { Vec3d t_z_dir = m_move_z_grabber.center - m_plane_center; const auto scale_z = t_z_dir.stableNorm(); const Vec3d target_z = m_plane_center + scale_z * t_z_dir.normalized(); Transform3d cube_mat_z = Geometry::translation_transform(target_z) * m_rotate_matrix * Geometry::scale_transform(fullsize); // m_move_z_grabber.m_matrix = cube_mat_z; std::array render_color = get_render_color(c_cube_z_move_id); render_glmodel(cube_z, render_color, view_matrix * m_move_z_grabber.m_matrix, projection_matrix); } if (is_render_x_grabber) { GLModel& cube_x = m_move_x_grabber.get_cube(); Vec3d t_x_dir = m_move_x_grabber.center - m_plane_center; const auto scale_x = t_x_dir.stableNorm(); const Vec3d target_x = m_plane_center + scale_x * t_x_dir.normalized(); Transform3d cube_mat_x = Geometry::translation_transform(target_x) * m_rotate_matrix * Geometry::scale_transform(fullsize); // m_move_x_grabber.m_matrix = cube_mat_x; std::array render_color = get_render_color(c_cube_x_move_id); render_glmodel(cube_x, render_color, view_matrix * m_move_x_grabber.m_matrix, projection_matrix); } // Should be placed at last, because GLGizmoRotate3D clears depth buffer GLGizmoRotate3D::set_center(m_plane_center); on_render_rotate_gizmos();//replace GLGizmoRotate3D::on_render(); } void GLGizmoAdvancedCut::on_render_rotate_gizmos() { if (m_is_dragging) { if (m_hover_id == 0) m_gizmos[X].render(); if ( m_hover_id == 1) m_gizmos[Y].render(); if (m_hover_id == 2) m_gizmos[Z].render(); } else { Transform3d rotate_tran = Geometry::translation_transform(m_plane_center) * m_rotate_matrix; for (int i = 0; i < 3; i++) { m_gizmos[i].set_custom_tran(rotate_tran); } m_gizmos[X].render(); m_gizmos[Y].render(); m_gizmos[Z].render(); } } void GLGizmoAdvancedCut::render_connectors() { ::glEnable(GL_DEPTH_TEST); if (cut_line_processing() || m_cut_mode != CutMode::cutPlanar || !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()) { clear_selection(); m_selected.resize(connectors.size(), false); } 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()); ColorRGBA render_color = CONNECTOR_DEF_COLOR; const bool looking_forward = is_looking_forward(); const Camera& camera = wxGetApp().plater()->get_camera(); const auto& view_matrix = camera.get_view_matrix(); const auto& projection_matrix = camera.get_projection_matrix(); 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 - c_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; deal_connector_pos_by_type(pos, height, connector.attribs.type, connector.attribs.style, looking_forward, m_connectors_editing, m_clp_normal); // BBL Transform3d translate_tf = Transform3d::Identity(); translate_tf.translate(pos); Transform3d scale_tf = Transform3d::Identity(); scale_tf.scale(Vec3f(connector.radius, connector.radius, height).cast()); const Transform3d view_model_matrix = view_matrix * translate_tf * m_rotate_matrix * scale_tf; render_glmodel(m_shapes[connector.attribs], render_color.get_data(), view_model_matrix, projection_matrix); } } void GLGizmoAdvancedCut::render_clipper_cut() { if (m_cut_mode == CutMode::cutTongueAndGroove) return; if (!m_connectors_editing) ::glDisable(GL_DEPTH_TEST); GLboolean cull_face = GL_FALSE; ::glGetBooleanv(GL_CULL_FACE, &cull_face); ::glDisable(GL_CULL_FACE); if (m_part_selection) { 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 GLGizmoAdvancedCut::render_cut_line() { if (!cut_line_processing() || m_cut_line_end.isApprox(Vec3d::Zero())) return; const auto& p_flat_shader = wxGetApp().get_shader("flat"); if (!p_flat_shader) { return; } if (!m_cut_line_model.is_initialized()) { GLModel::Geometry geo; geo.format.type = GLModel::PrimitiveType::Lines; geo.format.vertex_layout = GLModel::Geometry::EVertexLayout::P3; geo.add_vertex(Vec3f{ -0.5f, 0.0f, 0.0f }); geo.add_vertex(Vec3f{ 0.5f, 0.0f, 0.0f }); geo.add_line(0, 1); m_cut_line_model.init_from(std::move(geo)); } Transform3d model_matrix{ Transform3d::Identity() }; Vec3d the_vector = m_cut_line_end - m_cut_line_begin; const auto scale = the_vector.stableNorm(); Vec3d center = (m_cut_line_end + m_cut_line_begin) * 0.5f; Vec3d rotation_axis; double rotation_angle; Matrix3d rotation_matrix; Geometry::rotation_from_two_vectors(Vec3d(1.0f, 0.0f, 0.0f), the_vector, rotation_axis, rotation_angle, &rotation_matrix); Matrix4d final_rotation_matrix{ Matrix4d::Identity() }; for (int iColumn = 0; iColumn < 3; ++iColumn) { for (int jRow = 0; jRow < 3; ++jRow) { final_rotation_matrix.data()[4 * iColumn + jRow] = rotation_matrix.data()[3 * iColumn + jRow]; } } model_matrix = Geometry::translation_transform(center.cast()).matrix() * final_rotation_matrix * Geometry::scale_transform({ scale, scale, scale }).matrix(); m_cut_line_model.set_color({ 0.0f, 1.0f, 0.0f, 1.0f}); #ifdef __APPLE__ const auto &ogl_manager = wxGetApp().get_opengl_manager(); if (!ogl_manager) { return; } const auto &gl_info = ogl_manager->get_gl_info(); const auto formated_gl_version = gl_info.get_formated_gl_version(); if (formated_gl_version < 30) #endif { glsafe(::glEnable(GL_LINE_STIPPLE)); } glsafe(::glEnable(GL_DEPTH_TEST)); glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); wxGetApp().bind_shader(p_flat_shader); const Camera& camera = wxGetApp().plater()->get_camera(); const auto& view_matrix = camera.get_view_matrix(); const auto& proj_matrix = camera.get_projection_matrix(); p_flat_shader->set_uniform("projection_matrix", proj_matrix); p_flat_shader->set_uniform("view_model_matrix", view_matrix * model_matrix); m_cut_line_model.render_geometry(); wxGetApp().unbind_shader(); #ifdef __APPLE__ if (formated_gl_version < 30) #endif { glsafe(::glDisable(GL_LINE_STIPPLE)); } } void GLGizmoAdvancedCut::clear_selection() { m_selected.clear(); m_selected_count = 0; } void GLGizmoAdvancedCut::init_connector_shapes() { // --- ADDED OUR THREAD TO THE CACHE GENERATOR --- for (const CutConnectorType &type : {CutConnectorType::Snap, CutConnectorType::Dowel, CutConnectorType::Plug, CutConnectorType::Thread}) for (const CutConnectorStyle &style : {CutConnectorStyle::Frustum, CutConnectorStyle::Prizm}) for (const CutConnectorShape &shape : {CutConnectorShape::Circle, CutConnectorShape::Hexagon, CutConnectorShape::Square, CutConnectorShape::Triangle}) { CutConnectorAttributes attribs = {type, style, shape}; CutConnectorParas paras = {m_snap_space_proportion, m_snap_bulge_proportion}; const indexed_triangle_set its = ModelObject::get_connector_mesh(attribs, paras); m_shapes[attribs].init_from(its); } } void GLGizmoAdvancedCut::set_connectors_editing(bool connectors_editing) { if (m_connectors_editing == connectors_editing) return; m_connectors_editing = connectors_editing; if (m_connectors_editing) m_facet_picker.set_active(false); m_c->object_clipper()->set_behaviour(m_connectors_editing, m_connectors_editing, double(m_contour_width)); m_parent.request_extra_frame(); // todo: zhimin need a better method // after change render mode, need update for scene on_render(); } void GLGizmoAdvancedCut::reset_connectors() { m_c->selection_info()->model_object()->cut_connectors.clear(); clear_selection(); check_and_update_connectors_state(); } void GLGizmoAdvancedCut::update_connector_shape()//update mesh { CutConnectorAttributes attribs = { m_connector_type, CutConnectorStyle(m_connector_style), CutConnectorShape(m_connector_shape_id)}; CutConnectorParas paras = {m_snap_space_proportion, m_snap_bulge_proportion}; if (m_connector_type == CutConnectorType::Snap) { indexed_triangle_set its = ModelObject::get_connector_mesh(attribs, paras); m_shapes[attribs].reset(); m_shapes[attribs].init_from(its); } } void GLGizmoAdvancedCut::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(); } void GLGizmoAdvancedCut::select_all_connectors() { std::fill(m_selected.begin(), m_selected.end(), true); m_selected_count = int(m_selected.size()); } void GLGizmoAdvancedCut::unselect_all_connectors() { std::fill(m_selected.begin(), m_selected.end(), false); m_selected_count = 0; validate_connector_settings(); } void GLGizmoAdvancedCut::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_type == CutConnectorType::Undef) m_connector_type = CutConnectorType::Plug; if (m_connector_style == size_t(CutConnectorStyle::Undef)) m_connector_style = size_t(CutConnectorStyle::Prizm); if (m_connector_shape_id == size_t(CutConnectorShape::Undef)) m_connector_shape_id = size_t(CutConnectorShape::Circle); } bool GLGizmoAdvancedCut::add_connector(CutConnectors &connectors, const Vec2d &mouse_position) { m_add_connector_ok = false; if (!m_connectors_editing) return false; Vec3d pos; Vec3d pos_world; if (unproject_on_cut_plane(mouse_position.cast(), pos, pos_world)) { // --- THE MAGNET MATH (CLICK OVERRIDE) --- if (m_auto_center_connector && m_c && m_c->object_clipper()) { int contour_idx = m_c->object_clipper()->is_projection_inside_cut(pos_world); if (contour_idx >= 0) { Vec3d target_world; // THE HEURISTIC if (m_c->object_clipper()->get_number_of_contours() == 1) { target_world = m_plane_center; // Mathematical perfection! } else { // Complex shape approximation! std::vector contour_centers = m_c->object_clipper()->point_per_contour(); if (contour_idx < contour_centers.size()) { Vec3d center_3d = contour_centers[contour_idx]; double dist = (center_3d - m_plane_center).dot(m_plane_normal); target_world = center_3d - dist * m_plane_normal; } else { target_world = pos_world; // Fallback } } // THE FLAWLESS CONVERSION: Translate World to Local using the inverse rotation matrix pos = m_rotate_matrix.inverse() * (target_world - m_plane_center); } } // ---------------------------------------- Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Add connector"); unselect_all_connectors(); connectors.emplace_back(pos, m_rotate_matrix, m_connector_size * 0.5f, m_connector_depth_ratio, m_connector_size_tolerance, m_connector_depth_ratio_tolerance, 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()); m_parent.set_as_dirty(); check_and_update_connectors_state(); m_add_connector_ok = true; return true; } return false; } bool GLGizmoAdvancedCut::delete_selected_connectors() { CutConnectors &connectors = m_c->selection_info()->model_object()->cut_connectors; if (connectors.empty()) return false; Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Delete connector"); // 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()); m_parent.set_as_dirty(); return true; } bool GLGizmoAdvancedCut::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, Geometry::translation_transform(cur_pos) * m_rotate_matrix); 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 && 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 GLGizmoAdvancedCut::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]; Transform3d translate_tf = Transform3d::Identity(); translate_tf.translate(cur_pos); Transform3d scale_tf = Transform3d::Identity(); scale_tf.scale(Vec3f(cur_connector.radius, cur_connector.radius, cur_connector.height).cast()); const Transform3d matrix = translate_tf * m_rotate_matrix * scale_tf; const BoundingBoxf3 cur_tbb = m_shapes[cur_connector.attribs].get_bounding_box().transformed(matrix); // check if connector's bounding box is inside the object's bounding box if (!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 GLGizmoAdvancedCut::switch_to_mode(CutMode new_mode) { m_cut_mode = new_mode; if (m_cut_mode == CutMode::cutTongueAndGroove) { m_cut_to_parts = false;//into Groove function,cancel m_cut_to_parts } else { m_parent.set_use_dovetail_clip(false); } if (m_cut_mode != CutMode::cutPlanar) m_facet_picker.set_active(false); apply_color_clip_plane_colors(); if (auto oc = m_c->object_clipper()) { m_contour_width = m_cut_mode == CutMode::cutTongueAndGroove ? 0.f : 0.4f; oc->set_behaviour(m_connectors_editing, m_connectors_editing, double(m_contour_width)); // for debug } update_plane_model(); reset_cut_by_contours(); } void GLGizmoAdvancedCut::flip_cut_plane() { m_rotate_matrix = m_rotate_matrix * Geometry::rotation_transform(PI * Vec3d::UnitX()); m_transformed_bounding_box = transformed_bounding_box(m_plane_center, m_rotate_matrix); Plater::TakeSnapshot snapshot(wxGetApp().plater(), ("Flip cut plane")); m_start_dragging_m = m_rotate_matrix; update_clipper(); if (m_part_selection) { m_part_selection->turn_over_selection(); } if (m_cut_mode == CutMode::cutTongueAndGroove) reset_cut_by_contours(); } void GLGizmoAdvancedCut::update_plane_model() { m_plane.reset(); init_picking_models(); } static double get_grabber_mean_size(const BoundingBoxf3 &bb) { return (bb.size().x() + bb.size().y() + bb.size().z()) / 30.; } void GLGizmoAdvancedCut::init_picking_models() { if (!m_plane.is_initialized() && !m_connectors_editing) { if (m_cut_mode == CutMode::cutTongueAndGroove) { indexed_triangle_set its = its_make_groove_plane(m_groove, m_radius, m_groove_vertices); m_plane.init_from(its); // m_plane.mesh_raycaster = std::make_unique(std::make_shared(std::move(its)));//build error } else if (m_cut_mode == CutMode::cutPlanar) { const double cp_width = 0.02 * get_grabber_mean_size(m_bounding_box); indexed_triangle_set its = its_make_frustum_dowel((double) m_cut_plane_radius_koef * m_radius, cp_width, m_cut_plane_as_circle ? 180 : 4); m_plane.init_from(its); } } } bool GLGizmoAdvancedCut::has_valid_groove() const { if (m_cut_mode != CutMode::cutTongueAndGroove) return true; if (!has_valid_groove_shape()) 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; // Groove edge samples are filled when the plane mesh is built. if (m_groove_vertices.size() < 2) return true; const Transform3d cp_matrix = Geometry::translation_transform(m_plane_center) * m_rotate_matrix; if (!m_c->raycaster()) { if (!m_c->selection_info()) { m_c->update(get_requirements()); } wxBusyCursor wait; m_c->raycaster_ptr()->update(); } 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 && m_c->raycaster()) { for (size_t i = 0; i < m_c->raycaster()->raycasters().size(); i++) { if (m_c->raycaster()->raycasters()[i]->intersects_line(beg, end - beg, glvol->world_matrix())) { return true; } } } } if (!intersection) return false; } return true; } bool GLGizmoAdvancedCut::has_valid_groove_shape() const { if (m_cut_mode != CutMode::cutTongueAndGroove) return true; if (m_groove.depth <= 0.f || m_groove.width <= 0.f) return false; const float sin_flap = std::abs(std::sin(m_groove.flaps_angle)); return sin_flap > 0.01f; } bool GLGizmoAdvancedCut::has_valid_contour() const { const auto clipper = m_c->object_clipper(); return clipper && clipper->has_valid_contour(); } void GLGizmoAdvancedCut::reset_cut_by_contours() { update_buffer_data(); update_plane_normal(); m_part_selection.reset(new PartSelection()); if (m_cut_mode == CutMode::cutTongueAndGroove) { if (m_dragging || m_groove_editing || !has_valid_groove_shape()) return; update_dovetail_preview_clip(); toggle_model_objects_visibility(); } else { toggle_model_objects_visibility(); } } void GLGizmoAdvancedCut::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(); if (m_cut_mode == CutMode::cutTongueAndGroove) { // Dovetail preview is shader-based. Avoid running the expensive temporary // boolean here; the real perform_with_groove() still runs on confirmation. update_dovetail_preview_clip(); } else { wxBusyCursor wait; if (m_c->object_clipper()) { m_part_selection.reset(new PartSelection(model_objects[object_idx], get_cut_matrix(selection), instance_idx, m_plane_center, m_plane_normal, *m_c->object_clipper())); } } toggle_model_objects_visibility(); } void GLGizmoAdvancedCut::render_flip_plane_button(bool disable_pred /*=false*/) { ImGui::SameLine(); if (m_hover_id == c_plate_move_id) 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 == c_plate_move_id) ImGui::PopStyleColor(); } void GLGizmoAdvancedCut::toggle_model_objects_visibility(bool show_in_3d) { if (m_part_selection && m_part_selection->valid() && show_in_3d == false && (m_is_dragging == false || m_connectors_editing)) // BBL m_parent.toggle_model_objects_visibility(false); else // if (!m_part_selection.valid()) { const Selection & selection = m_parent.get_selection(); const ModelObjectPtrs &model_objects = selection.get_model()->objects; const int idx = selection.get_object_idx(); if (idx < 0) { return; } if (idx >= model_objects.size()) { return; } m_parent.toggle_model_objects_visibility(true, model_objects[idx], selection.get_instance_idx()); } } void GLGizmoAdvancedCut::deal_connector_pos_by_type( Vec3d &pos, float &height, CutConnectorType connector_type, CutConnectorStyle connector_style, bool looking_forward, bool is_edit, const Vec3d &clp_normal) {//deal pos and height,out :pos and height if (connector_type == CutConnectorType::Dowel && connector_style == CutConnectorStyle::Prizm) { if (is_edit) { 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; } } void GLGizmoAdvancedCut::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_cut_mode = CutMode::cutPlanar; invalidate_cut_plane(); 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); // update m_plane_center else set_center_pos(m_bb_center); reset_cut_by_contours(); apply_color_clip_plane_colors(); m_contour_width = m_cut_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; //// 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(); 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 GLGizmoAdvancedCut::check_and_update_connectors_state() { m_info_stats.invalidate(); m_invalid_connectors_idxs.clear(); if (m_cut_mode != CutMode::cutPlanar) return; if (m_c == nullptr || m_c->selection_info() == nullptr) { 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 GLGizmoAdvancedCut::set_center(const Vec3d ¢er, bool update_tbb) { set_center_pos(center, update_tbb); check_and_update_connectors_state(); update_clipper(); } bool GLGizmoAdvancedCut::set_center_pos(const Vec3d ¢er_pos, bool update_tbb) { BoundingBoxf3 tbb = m_transformed_bounding_box; if (update_tbb) { Vec3d normal = m_rotate_matrix.inverse() * Vec3d(m_plane_center - center_pos); tbb.translate(normal); } bool can_set_center_pos = false; { double limit_val = 0.5; //&& (tbb.max.x() > m_transformed_bounding_box.min.x() && tbb.min.x() > m_transformed_bounding_box.max.x() 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; return true; } return false; } void GLGizmoAdvancedCut::invalidate_cut_plane() { m_rotate_matrix = 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 GLGizmoAdvancedCut::rotate_vec3d_around_plane_center(Vec3d &vec, const Transform3d &rotate_matrix, const Vec3d ¢er) { vec = Geometry::Transformation(Geometry::translation_transform(center) * rotate_matrix * Geometry::translation_transform(-center)).get_matrix() * vec; } Transform3d GLGizmoAdvancedCut::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 Geometry::translation_transform(cut_center_offset) * m_rotate_matrix; } void GLGizmoAdvancedCut::update_buffer_data() { m_buffered_rotation = {Geometry::rad2deg(m_rotation(0)), Geometry::rad2deg(m_rotation(1)), Geometry::rad2deg(m_rotation(2))}; m_buffered_movement = m_movement; m_buffered_height = m_plane_center.z(); } bool GLGizmoAdvancedCut::render_cut_mode_combo(double label_width, float item_width) { ImGui::AlignTextToFramePadding(); size_t selection_idx = int(m_cut_mode); std::vector modes = {_u8L("Planar"), _u8L("Dovetail")}; bool is_changed = false; ImGuiWrapper::push_combo_style(m_parent.get_scale()); if (render_combo(_u8L("Mode"), modes, selection_idx, label_width, item_width)) { is_changed = true; Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Change cut mode"); switch_to_mode((CutMode) selection_idx); check_and_update_connectors_state(); } ImGuiWrapper::pop_combo_style(); return is_changed; } void GLGizmoAdvancedCut::render_color_marker(float size, const ColorRGBA &color) { ImGui::SameLine(); const float radius = 0.5f * size; ImVec2 pos = ImGui::GetCurrentWindow()->DC.CursorPos; pos.x += 3 * m_imgui->scaled(1 / 15.0f); pos.y += 1.25f * radius; ImGui::GetCurrentWindow()->DrawList->AddNgonFilled(pos, radius, ImGuiWrapper::to_ImU32(color), 6); } void GLGizmoAdvancedCut::render_cut_plane_input_window(float x, float y, float bottom_limit) { // float unit_size = m_imgui->get_style_scaling() * 48.0f; float space_size = m_imgui->get_style_scaling() * 8; float movement_cap = m_imgui->calc_text_size(_L("Movement:")).x; float rotate_cap = m_imgui->calc_text_size(_L("Rotate")).x; float groove_angle_size = m_imgui->calc_text_size(_L("Groove Angle")).x; float caption_size = std::max(movement_cap, groove_angle_size) + 2 * space_size; m_label_width = caption_size + 1 * space_size; m_imperial_units = wxGetApp().app_config->get("use_inches") == "1"; unsigned int current_active_id = ImGui::GetActiveID(); char buf[3][64]; float buf_size[3]; float vec_max = 0, unit_size = 0; for (int i = 0; i < 3; i++) { ImGui::DataTypeFormatString(buf[i], IM_ARRAYSIZE(buf[i]), ImGuiDataType_Double, (void *) &m_buffered_rotation[i], "%.2f"); buf_size[i] = ImGui::CalcTextSize(buf[i]).x; vec_max = std::max(buf_size[i], vec_max); } float buf_size_max = ImGui::CalcTextSize("-100.00").x; if (vec_max < buf_size_max) { unit_size = buf_size_max + ImGui::GetStyle().FramePadding.x * 2.0f; } else { unit_size = vec_max + ImGui::GetStyle().FramePadding.x * 2.0f; } bool has_connectors = false; if (m_c) { const auto& p_selection_info = m_c->selection_info(); if (p_selection_info) { const auto& p_model_object = p_selection_info->model_object(); if (p_model_object) { const auto& t_commectors = p_model_object->cut_connectors; has_connectors = !t_commectors.empty(); } } } m_imgui->disabled_begin(has_connectors); if (render_cut_mode_combo(caption_size + 1 * space_size, 4 * unit_size + 0.5 * space_size)) { ; } ImGui::Separator(); m_imgui->disabled_end(); #if 0 // hide Pick-face entry // Pick-face mode is planar-cut only; the groove mode has its own plane state machine. const bool pick_face_available = (m_cut_mode == CutMode::cutPlanar) && !m_connectors_editing; m_imgui->disabled_begin(!pick_face_available); // Keep the button looking pressed while armed - it is a mode, and the only other // cue that it is on is the facet highlight, which needs the cursor over the model. const bool picking = m_facet_picker.is_active(); if (picking) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetColorU32(ImGuiCol_ButtonActive)); if (m_imgui->button(_L("Pick face"))) m_facet_picker.set_active(!picking); if (picking) ImGui::PopStyleColor(); m_imgui->disabled_end(); if (ImGui::IsItemHovered()) m_imgui->tooltip(_L("Click a face of the model to set the cut plane. The plane lands flush with that face; use Movement to offset it."), ImGui::GetFontSize() * 20.0f); if (m_facet_picker.is_active()) { ImGui::SameLine(); m_imgui->text(_L("Click a face of the model.")); } ImGui::Separator(); #endif ImGui::PushItemWidth(caption_size); ImGui::Dummy(ImVec2(caption_size, -1)); ImGui::SameLine(caption_size + 1 * space_size); ImGui::PushItemWidth(unit_size); ImGui::TextAlignCenter("X"); ImGui::SameLine(caption_size + 1 * unit_size + 2 * space_size); ImGui::PushItemWidth(unit_size); ImGui::TextAlignCenter("Y"); ImGui::SameLine(caption_size + 2 * unit_size + 3 * space_size); ImGui::PushItemWidth(unit_size); ImGui::TextAlignCenter("Z"); ImGui::AlignTextToFramePadding(); // Rotation input box ImGui::PushItemWidth(caption_size); m_imgui->text(_L("Rotation") + " "); ImGui::SameLine(caption_size + 1 * space_size); ImGui::PushItemWidth(unit_size); for (int i = 0; i < m_buffered_rotation.size(); i++) { if (abs(m_buffered_rotation[i]) < 1e-3) { m_buffered_rotation[i] = 0; } } ImGui::BBLInputDouble("##cut_rotation_x", &m_buffered_rotation[0], 0.0f, 0.0f, "%.2f"); ImGui::SameLine(caption_size + 1 * unit_size + 2 * space_size); ImGui::PushItemWidth(unit_size); ImGui::BBLInputDouble("##cut_rotation_y", &m_buffered_rotation[1], 0.0f, 0.0f, "%.2f"); ImGui::SameLine(caption_size + 2 * unit_size + 3 * space_size); ImGui::PushItemWidth(unit_size); ImGui::BBLInputDouble("##cut_rotation_z", &m_buffered_rotation[2], 0.0f, 0.0f, "%.2f"); if (m_last_active_item_imgui != current_active_id && (std::abs(Geometry::rad2deg(m_rotation(0)) - m_buffered_rotation(0)) > EPSILON || std::abs(Geometry::rad2deg(m_rotation(1)) - m_buffered_rotation(1)) > EPSILON || std::abs(Geometry::rad2deg(m_rotation(2)) - m_buffered_rotation(2)) > EPSILON )) { m_rotation(0) = Geometry::deg2rad(m_buffered_rotation(0)); m_rotation(1) = Geometry::deg2rad(m_buffered_rotation(1)); m_rotation(2) = Geometry::deg2rad(m_buffered_rotation(2)); Geometry::Transformation tran; tran.set_rotation(m_rotation); update_plate_normal_boundingbox_clipper(tran.get_matrix()); reset_cut_by_contours(); Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Rotate cut plane"); m_start_dragging_m = m_rotate_matrix; // for takeshot } ImGui::Separator(); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 10.0f)); // Movement input box ImGui::PushItemWidth(caption_size); ImGui::AlignTextToFramePadding(); m_imgui->text(_L("Movement") + " "); ImGui::SameLine(caption_size + 1 * space_size); ImGui::PushItemWidth(3 * unit_size + 2 * space_size); ImGui::BBLInputDouble("##cut_movement", &m_buffered_movement, 0.0f, 0.0f, "%.2f"); if (m_last_active_item_imgui != current_active_id && std::abs(m_buffered_movement - m_movement) > EPSILON) { // update absolute height update_plate_center(Axis::Z, m_buffered_movement, false); reset_cut_by_contours(); m_buffered_movement = m_movement = 0.0; m_buffered_height = m_plane_center.z();//update m_buffered_height Plater::TakeSnapshot snapshot(wxGetApp().plater(), "set z along z axis for cut plane"); m_ar_plane_center = m_plane_center; } // height input box ImGui::PushItemWidth(caption_size); ImGui::AlignTextToFramePadding(); // only allow setting height when cut plane is horizontal Vec3d plane_normal = get_plane_normal(); m_imgui->disabled_begin(std::abs(plane_normal(0)) > EPSILON || std::abs(plane_normal(1)) > EPSILON); m_imgui->text(_L("Height") + " "); ImGui::SameLine(caption_size + 1 * space_size); ImGui::PushItemWidth(3 * unit_size + 2 * space_size); ImGui::BBLInputDouble("##cut_height", &m_buffered_height, 0.0f, 0.0f, "%.2f"); if (m_last_active_item_imgui != current_active_id && std::abs(m_buffered_height - m_plane_center.z()) > EPSILON) { // Height is the absolute world-Z of the cut plane center, so set world Z directly. Vec3d new_center = m_plane_center; new_center.z() = m_buffered_height; set_center(new_center, true); reset_cut_by_contours(); Plater::TakeSnapshot snapshot(wxGetApp().plater(), "set height for cut plane"); m_ar_plane_center = m_plane_center; } ImGui::PopStyleVar(1); m_imgui->disabled_end(); ImGui::Separator(); if (m_cut_mode == CutMode::cutPlanar) { m_imgui->disabled_begin(!m_keep_upper || !m_keep_lower || m_cut_to_parts); if (m_imgui->button(has_connectors ? _L("Edit connectors") : _L("Add connectors"))) set_connectors_editing(true); m_imgui->disabled_end(); } else if (m_cut_mode == CutMode::cutTongueAndGroove) { m_is_slider_editing_done = false; m_imgui->text(_L("Groove") + ": "); // ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, m_labels_map["Groove"] + ": "); bool is_changed{false}; is_changed |= render_slider_double_input(_u8L("Depth"), m_groove.depth, m_groove.depth_tolerance); is_changed |= render_slider_double_input(_u8L("Width"), m_groove.width, m_groove.width_tolerance); is_changed |= render_slider_double_input_by_format(_u8L("Flap Angle"), m_groove.flaps_angle, 30.f, 120.f, DoubleShowType::DEGREE); is_changed |= render_slider_double_input_by_format(_u8L("Groove Angle"), m_groove.angle, 0.f, 15.f, DoubleShowType::DEGREE); if (is_changed) { update_plane_model(); reset_cut_by_contours(); } if (m_is_slider_editing_done) { m_groove_editing = false; reset_cut_by_contours(); } } ImGui::Separator(); auto render_part_action_line = [this, space_size,has_connectors](const wxString &label, const wxString &suffix, bool &keep_part, bool &place_on_cut_part, bool &rotate_part) { bool keep = true; m_imgui->disabled_begin(has_connectors || m_cut_to_parts); ImGui::AlignTextToFramePadding(); auto text = m_cut_to_parts ? _L("Part") : _L("Object") + label; m_imgui->bbl_checkbox(text, has_connectors ? keep : keep_part); float marker_size = m_imgui->calc_text_size(text).y; render_color_marker(marker_size, suffix == "##upper" ? UPPER_PART_COLOR : LOWER_PART_COLOR); m_imgui->disabled_end(); m_imgui->disabled_begin(!keep_part || m_cut_to_parts); float text_size = m_imgui->calc_text_size(text).x; text_size += m_imgui->scaled(2.5f); float checkbox_size = m_imgui->calc_text_size(text).y; float new_label_width = checkbox_size + text_size + 2 * space_size; ImGui::SameLine(new_label_width); bool is_keep = !place_on_cut_part && !rotate_part; if (m_imgui->bbl_checkbox(_L("Keep orientation") + suffix, is_keep)){ rotate_part = false; place_on_cut_part = false; } ImGui::SameLine(); if (m_imgui->bbl_checkbox(_L("Place on cut") + suffix, place_on_cut_part)) { // in bbl_checkbox have done//place_on_cut_part = !place_on_cut_part; rotate_part = false; } ImGui::SameLine(); if (m_imgui->bbl_checkbox(_L("Flip") + suffix, rotate_part)) { //in bbl_checkbox have done://rotate_part = !rotate_part; place_on_cut_part = false; } m_imgui->disabled_end(); }; m_imgui->text(_L("After cut") + ": "); render_part_action_line( "A", "##upper", m_keep_upper, m_place_on_cut_upper, m_rotate_upper); render_part_action_line( "B", "##lower", m_keep_lower, m_place_on_cut_lower, m_rotate_lower); if (m_part_selection && m_part_selection->has_modified_cut_parts()) { m_cut_to_parts = false; } m_imgui->disabled_begin(has_connectors || (m_part_selection && m_part_selection->has_modified_cut_parts() && m_part_selection->valid()) || m_cut_mode == CutMode::cutTongueAndGroove); m_imgui->bbl_checkbox(_L("Cut to parts"), m_cut_to_parts); if (m_cut_to_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(); #if 0 // Auto segment input ImGui::PushItemWidth(m_imgui->get_style_scaling() * 150.0); m_imgui->checkbox(_L("Auto Segment"), m_do_segment); m_imgui->disabled_begin(!m_do_segment); ImGui::InputDouble("smoothing_alpha", &m_segment_smoothing_alpha, 0.0f, 0.0f, "%.2f"); m_segment_smoothing_alpha = std::max(0.1, std::min(100.0, m_segment_smoothing_alpha)); ImGui::InputInt("segment number", &m_segment_number); m_segment_number = std::max(1, m_segment_number); m_imgui->disabled_end(); ImGui::Separator(); #endif ImGui::Separator(); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 10.0f)); float get_cur_y = ImGui::GetContentRegionMax().y + ImGui::GetFrameHeight() + y; show_tooltip_information(x, get_cur_y); float f_scale = m_parent.get_main_toolbar_scale(); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6.0f, 4.0f * f_scale)); ImGui::SameLine(); // Cut button m_imgui->disabled_begin(!can_perform_cut()); if (m_imgui->button(_L("Perform cut")) && !m_perform_cut_requested) { // This window is rendered from inside GLCanvas3D::render(), which refuses to re-enter // itself (m_in_render). perform_cut() then pumps the event loop for its progress dialog, // so every repaint yielded from there would return immediately and leave the 3D view // frozen on a stale frame. Run it from the event loop instead, outside the render pass. m_perform_cut_requested = true; wxGetApp().plater()->CallAfter([this]() { m_perform_cut_requested = false; // The gizmo may have been closed or the selection changed before this ran. if (m_state == On && can_perform_cut()) perform_cut(m_parent.get_selection()); }); } m_imgui->disabled_end(); ImGui::SameLine(); const bool reset_clicked = m_imgui->button(_L("Reset")); ImGui::PopStyleVar(2); if (reset_clicked) { reset_all(); } m_last_active_item_imgui = current_active_id; } void GLGizmoAdvancedCut::init_connectors_input_window_data() { if (!m_c) { return; } const auto p_selection_info = m_c->selection_info(); if (!p_selection_info) { return; } const auto& p_model_object = p_selection_info->model_object(); if (!p_model_object) { return; } CutConnectors &connectors = p_model_object->cut_connectors; float connectors_cap = m_imgui->calc_text_size(_L("Connectors")).x; float type_cap = m_imgui->calc_text_size(_L("Type")).x; float style_cap = m_imgui->calc_text_size(_L("Style")).x; float shape_cap = m_imgui->calc_text_size(_L("Shape")).x; float depth_ratio_cap = m_imgui->calc_text_size(_L("Depth ratio")).x; float size_cap = m_imgui->calc_text_size(_L("Size")).x; float max_lable_size = std::max(std::max(std::max(connectors_cap, type_cap), std::max(style_cap, shape_cap)), std::max(depth_ratio_cap, size_cap)); m_label_width = double(max_lable_size + 3 + ImGui::GetStyle().WindowPadding.x); if (m_connectors_editing && m_selected_count > 0) { float depth_ratio {UndefFloat}; float depth_ratio_tolerance {UndefFloat}; float radius {UndefFloat}; float radius_tolerance {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; 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 (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 = radius_tolerance; m_connector_type = type; m_connector_style = size_t(style); m_connector_shape_id = size_t(shape); } } void GLGizmoAdvancedCut::render_connectors_input_window(float x, float y, float bottom_limit) { if (!m_c) { return; } const auto p_selection_info = m_c->selection_info(); if (!p_selection_info) { return; } const auto& p_model_object = p_selection_info->model_object(); if (!p_model_object) { return; } CutConnectors &connectors = p_model_object->cut_connectors; // update when change input window m_imgui->set_requires_extra_frame(); ImGui::AlignTextToFramePadding(); m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Connectors")); m_imgui->disabled_begin(connectors.empty()); ImGui::SameLine(m_label_width); if (render_reset_button("connectors", _u8L("Remove connectors"))) reset_connectors(); m_imgui->disabled_end(); render_flip_plane_button(m_connectors_editing && connectors.empty()); m_imgui->text(_L("Type")); ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.00f, 0.00f, 0.00f, 1.00f)); 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); type_changed |= render_connect_type_radio_button(CutConnectorType::Thread); if (type_changed) apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.type = CutConnectorType(m_connector_type); }); ImGui::PopStyleColor(1); std::vector connector_styles = {_u8L("Prizm"), _u8L("Frustum")}; std::vector connector_shapes = { _u8L("Triangle"), _u8L("Square"), _u8L("Hexagon"), _u8L("Circle") }; // 1. THE LOCKDOWN: Added Thread to the disabled list so the dropdown greys out! m_imgui->disabled_begin(m_connector_type == CutConnectorType::Dowel || m_connector_type == CutConnectorType::Snap || m_connector_type == CutConnectorType::Thread); if (type_changed && m_connector_type == CutConnectorType::Dowel) { m_connector_style = size_t(CutConnectorStyle::Prizm); apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.style = CutConnectorStyle(m_connector_style); }); } else if (type_changed && m_connector_type == CutConnectorType::Thread) { // 2. THE FORCE: Automatically sets the UI value to Circle (index 3) m_connector_shape_id = size_t(CutConnectorShape::Circle); apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.shape = CutConnectorShape(m_connector_shape_id); }); } ImGuiWrapper::push_combo_style(m_parent.get_scale()); if (render_combo(_u8L("Style"), connector_styles, m_connector_style, m_label_width, m_editing_window_width)) apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.style = CutConnectorStyle(m_connector_style); }); ImGuiWrapper::pop_combo_style(); m_imgui->disabled_end(); // 1. THE SHAPE LOCKDOWN: Grey out the box for both Snap AND Thread m_imgui->disabled_begin(m_connector_type == CutConnectorType::Snap || m_connector_type == CutConnectorType::Thread); // 2. THE FORCE: If they just clicked Snap OR Thread, force the dropdown to Circle if (type_changed && (m_connector_type == CutConnectorType::Snap || m_connector_type == CutConnectorType::Thread)) { m_connector_shape_id = int(CutConnectorShape::Circle); apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.shape = CutConnectorShape(m_connector_shape_id); }); } ImGuiWrapper::push_combo_style(m_parent.get_scale()); if (render_combo(_u8L("Shape"), connector_shapes, m_connector_shape_id, m_label_width, m_editing_window_width)) apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.shape = CutConnectorShape(m_connector_shape_id); }); ImGuiWrapper::pop_combo_style(); m_imgui->disabled_end(); if (render_slider_double_input(_u8L("Depth ratio"), m_connector_depth_ratio, m_connector_depth_ratio_tolerance)) 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(_u8L("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 = m_connector_size_tolerance; }); // --- NEW: MIDDLE OF GEOMETRY CHECKBOX --- m_imgui->bbl_checkbox(_L("Middle of geometry"), m_auto_center_connector); // ---------------------------------------- if (m_connector_type == CutConnectorType::Snap) { m_imgui->text(_L("Snap global parameters") +": "); const std::string format = "%.0f %%"; bool is_changed = false; if (render_slider_double_input_by_format(_u8L("Bulge"), m_snap_bulge_proportion, 5.f, 100.f * m_snap_space_proportion, DoubleShowType::PERCENTAGE)) { is_changed = true; apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].paras.snap_bulge_proportion = m_snap_bulge_proportion; }); } if (render_slider_double_input_by_format(_u8L("Gap"), m_snap_space_proportion, 10.f, 50.f, DoubleShowType::PERCENTAGE)) { is_changed = true; apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].paras.snap_space_proportion = m_snap_space_proportion; }); } if (is_changed) { update_connector_shape(); } } ImGui::Separator(); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 10.0f)); float get_cur_y = ImGui::GetContentRegionMax().y + ImGui::GetFrameHeight() + y; show_tooltip_information(x, get_cur_y); float f_scale = m_parent.get_main_toolbar_scale(); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6.0f, 4.0f * f_scale)); ImGui::SameLine(); if (m_imgui->button(_L("Confirm connectors"))) { unselect_all_connectors(); set_connectors_editing(false); } ImGui::SameLine(m_label_width + m_editing_window_width - m_imgui->calc_text_size(_L("Cancel")).x - m_imgui->get_style_scaling() * 8); if (m_imgui->button(_L("Cancel"))) { reset_connectors(); set_connectors_editing(false); } ImGui::PopStyleVar(2); } void GLGizmoAdvancedCut::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 - " + std::to_string(m_info_stats.outside_cut_contour) + (m_info_stats.outside_cut_contour == 1 ? _L("connector is out of cut contour") : _L("connectors are out of cut contour")); if (m_info_stats.outside_bb > size_t(0)) out += "\n - " + std::to_string(m_info_stats.outside_bb) + (m_info_stats.outside_bb == 1 ? _L("connector is out of object") : _L("connectors are out of object")); if (m_info_stats.is_overlap) out += "\n - " + _L("Some connectors are overlapped"); m_imgui->warning_text(out); } if (!m_keep_upper && !m_keep_lower) { m_imgui->warning_text(/*wxString(ImGui::WarningMarkerSmall)*/ _L("Warning") + ": " + _L("Invalid state. \nNo one part is selected to keep after cut")); } } bool GLGizmoAdvancedCut::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}); bool revert = m_imgui->button(wxString(ImGui::RevertBtn)); ImGui::PopStyleColor(3); if (ImGui::IsItemHovered()) m_imgui->tooltip(tooltip.c_str(), ImGui::GetFontSize() * 20.0f); ImGui::PopStyleVar(); return revert; } bool GLGizmoAdvancedCut::render_connect_type_radio_button(CutConnectorType type) { ImGui::SameLine(type == CutConnectorType::Plug ? m_label_width : (type == CutConnectorType::Dowel ? 2 * m_label_width : (type == CutConnectorType::Snap ? 3 * m_label_width : 4 * m_label_width))); ImGui::PushItemWidth(m_control_width); wxString radio_name; switch (type) { case CutConnectorType::Plug: radio_name = _L("Plug"); break; case CutConnectorType::Dowel: radio_name = _L("Dowel"); break; case CutConnectorType::Snap: radio_name = _L("Snap"); break; case CutConnectorType::Thread: radio_name = _L("Thread"); break; default: break; } if (m_imgui->radio_button(radio_name, m_connector_type == type)) { m_connector_type = type; return true; } return false; } bool GLGizmoAdvancedCut::render_slider_double_input(const std::string &label, float &value_in, float &tolerance_in) { // -------- [ ] -------- [ ] // slider_with + item_in_gap + first_input_width + item_out_gap + slider_with + item_in_gap + second_input_width double slider_with = 0.24 * m_editing_window_width; double item_in_gap = 0.01 * m_editing_window_width; double item_out_gap = 0.04 * m_editing_window_width; double first_input_width = 0.37 * m_editing_window_width; double second_input_width = 0.37 * m_editing_window_width; #ifdef __APPLE__ first_input_width = 0.5 * m_editing_window_width; second_input_width = 0.5 * m_editing_window_width; #endif ImGui::AlignTextToFramePadding(); m_imgui->text(label); ImGui::SameLine(m_label_width); ImGui::PushItemWidth(slider_with); double left_width = m_label_width + slider_with + item_in_gap; float value = value_in; if (m_imperial_units) value *= float(units_mm_to_in); float old_val = value; constexpr float UndefMinVal = -0.1f; const BoundingBoxf3 bbox = bounding_box(); float mean_size = float((bbox.size().x() + bbox.size().y() + bbox.size().z()) / 2.0); float min_size = value_in < 0.f ? UndefMinVal : 1.f; if (m_imperial_units) { mean_size *= float(units_mm_to_in); min_size *= float(units_mm_to_in); } std::string format = value_in < 0.f ? " " : m_imperial_units ? "%.4f " + _u8L("in") : "%.2f " + _u8L("mm"); m_imgui->bbl_slider_float_style(("##" + label).c_str(), &value, min_size, mean_size, format.c_str()); ImGui::SameLine(left_width); ImGui::PushItemWidth(first_input_width); ImGui::BBLDragFloat(("##input_" + label).c_str(), &value, 0.05f, min_size, mean_size, format.c_str()); value_in = value * float(m_imperial_units ? units_in_to_mm : 1.0); left_width += (first_input_width + item_out_gap); ImGui::SameLine(left_width); ImGui::PushItemWidth(slider_with); float tolerance = tolerance_in; if (m_imperial_units) tolerance *= float(units_mm_to_in); float old_tolerance = tolerance; //std::string format_t = tolerance_in < 0.f ? " " : "%.f %%"; float min_tolerance = tolerance_in < 0.f ? UndefMinVal : 0.f; m_imgui->bbl_slider_float_style(("##tolerance_" + label).c_str(), &tolerance, min_tolerance, 2.f, format.c_str(), 1.f, true, _L("Tolerance")); left_width += (slider_with + item_in_gap); ImGui::SameLine(left_width); ImGui::PushItemWidth(second_input_width); ImGui::BBLDragFloat(("##tolerance_input_" + label).c_str(), &tolerance, 0.05f, min_tolerance, 2.f, format.c_str()); tolerance_in = tolerance * float(m_imperial_units ? units_in_to_mm : 1.0); return !is_approx(old_val, value) || !is_approx(old_tolerance, tolerance); } bool GLGizmoAdvancedCut::render_slider_double_input_by_format(const std::string &label, float &value_in, float value_min, float value_max, DoubleShowType show_type) { // slider_with + item_in_gap + first_input_width + item_out_gap double slider_with = 0.24 * m_editing_window_width; double item_in_gap = 0.01 * m_editing_window_width; double item_out_gap = 0.01 * m_editing_window_width; double first_input_width = 0.29 * m_editing_window_width; ImGui::AlignTextToFramePadding(); m_imgui->text(label); ImGui::SameLine(m_label_width); ImGui::PushItemWidth(slider_with); double left_width = m_label_width + slider_with + item_in_gap; float old_val = value_in; // (show_type == DoubleShowType::Normal) float value = value_in; // (show_type == DoubleShowType::Normal) std::string format = "%.0f"; if (show_type == DoubleShowType::PERCENTAGE) { format = "%.0f %%"; old_val = value_in; value = value_in * 100; } else if (show_type == DoubleShowType::DEGREE) { format = "%.0f " + _u8L("°"); old_val = value_in; value = Geometry::rad2deg(value_in); } if (m_imgui->bbl_slider_float_style(("##" + label).c_str(), &value, value_min, value_max, format.c_str())) { if (show_type == DoubleShowType::PERCENTAGE) { value_in = value * 0.01f; } else if (show_type == DoubleShowType::DEGREE) { value_in = Geometry::deg2rad(value); } else { //(show_type == DoubleShowType::Normal) value_in = value; } } ImGui::SameLine(left_width); ImGui::PushItemWidth(first_input_width); if (ImGui::BBLDragFloat(("##input_" + label).c_str(), &value, 0.05f, value_min, value_max, format.c_str())) { if (show_type == DoubleShowType::PERCENTAGE) { value_in = value * 0.01f; } else if (show_type == DoubleShowType::DEGREE) { value_in = Geometry::deg2rad(value); } else { //(show_type == DoubleShowType::Normal) value_in = value; } } return !is_approx(old_val, value_in); } bool GLGizmoAdvancedCut::cut_line_processing() const { return m_cut_line_begin != Vec3d::Zero(); } void GLGizmoAdvancedCut::discard_cut_line_processing() { m_cut_line_begin = m_cut_line_end = Vec3d::Zero(); } bool GLGizmoAdvancedCut::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_static(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_cut_line_begin = pt; m_cut_line_end = pt; return true; } if (cut_line_processing()) { if (m_cut_mode == CutMode::cutTongueAndGroove) m_groove_editing = true; m_cut_line_end = pt; if (action == SLAGizmoEventType::LeftUp) { Vec3d line_dir = m_cut_line_end - m_cut_line_begin; if (line_dir.norm() < 3.0) { discard_cut_line_processing(); 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(), "Cut by line"); m_transformed_bounding_box = new_tbb; set_center(new_plane_center); m_start_dragging_m = m_rotate_matrix = m; m_plane_normal = m_rotate_matrix * Vec3d::UnitZ(); m_ar_plane_center = m_plane_center; reset_cut_by_contours(); } discard_cut_line_processing(); if (m_cut_mode == CutMode::cutTongueAndGroove) { m_groove_editing = false; if (new_tbb.contains(trans_center_pos)) { reset_cut_by_contours(); } } } else if (action == SLAGizmoEventType::Moving) { this->set_dirty(); } return true; } return false; } PartSelection::PartSelection( const ModelObject *mo, const Transform3d &cut_matrix, int instance_idx_in, const Vec3d ¢er, 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--) { auto look = volumes[id]->is_model_part(); if (volumes[id]->is_splittable() && volumes[id]->is_model_part()) // we have to split just solid volumes volumes[id]->split(1); } const Vec3d inst_offset = model_object()->instances[m_instance_idx]->get_offset(); int i = 0; m_cut_parts.resize(volumes.size()); for (const ModelVolume *volume : volumes) { assert(volume != nullptr); m_cut_parts[i].is_modifier = !volume->is_model_part(); m_cut_parts[i].is_up_part = false; if (m_cut_parts[i].raycaster) { delete m_cut_parts[i].raycaster; } m_cut_parts[i].raycaster = new MeshRaycaster(volume->mesh()); m_cut_parts[i].glmodel.reset(); m_cut_parts[i].glmodel.init_from(volume->mesh_ptr()->its); m_cut_parts[i].trans = Geometry::translation_transform(inst_offset) * model_object()->volumes[i]->get_matrix(); // 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) > 0.01) { // 0.01 mm is enough m_cut_parts[i].is_up_part = p > 0.; break; } } i++; } m_back_cut_parts_state.resize(m_cut_parts.size()); for (size_t i = 0; i < m_cut_parts.size(); i++) { m_back_cut_parts_state[i] = m_cut_parts[i].is_up_part; } // 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_cut_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_idx < pts.size(); ++pt_idx) { const Vec3d &pt = pts[pt_idx]; const Vec3d dir = (center - pt).dot(normal) * normal; m_contour_points.emplace_back(dir + pt); // the result is in world coordinates. // Now, cast a ray from every contour point and see which volumes of the ones above // the plane are hit from the inside. for (size_t part_id = 0; part_id < m_cut_parts.size(); ++part_id) { const sla::IndexedMesh &aabb = m_cut_parts[part_id].raycaster->get_aabb_mesh(); const Transform3d & tr = (Geometry::translation_transform(model_object()->instances[m_instance_idx]->get_offset()) * Geometry::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); auto 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 PartSelection::PartSelection(const ModelObject *object, int instance_idx_in) : m_instance_idx(instance_idx_in) { add_object(object); const ModelVolumePtrs &volumes = model_object()->volumes; const Vec3d inst_offset = model_object()->instances[m_instance_idx]->get_offset(); int i = 0; m_cut_parts.resize(volumes.size()); for (const ModelVolume *volume : volumes) { assert(volume != nullptr); m_cut_parts[i].is_modifier = !volume->is_model_part(); if (m_cut_parts[i].raycaster) { delete m_cut_parts[i].raycaster; } m_cut_parts[i].raycaster = new MeshRaycaster(volume->mesh()); m_cut_parts[i].glmodel.reset(); m_cut_parts[i].glmodel.init_from(volume->mesh_ptr()->its); m_cut_parts[i].trans = Geometry::translation_transform(inst_offset) * model_object()->volumes[i]->get_matrix(); // Now check whether this part is below or above the plane. m_cut_parts[i].is_up_part = volume->is_from_upper(); i++; } m_valid = true; } void PartSelection::part_render(const Vec3d *cut_center, const Vec3d *normal) { if (!valid()) return; const Camera& camera = wxGetApp().plater()->get_camera(); const auto& view_matrix = camera.get_view_matrix(); const auto& projection_matrix = camera.get_projection_matrix(); glEnable(GL_DEPTH_TEST); for (size_t id = 0; id < m_cut_parts.size(); ++id) { // m_parts.size() test bool is_looking_forward = true; auto is_at_normal_dir = false; if (cut_center && normal) { auto part_center =m_cut_parts[id].trans * m_cut_parts[id].glmodel.get_bounding_box().center(); is_at_normal_dir=(*cut_center - part_center).normalized().dot(*normal) > 0 ; Vec3d valid_normal = is_at_normal_dir ? *normal : -*normal; is_looking_forward = camera.get_dir_forward().dot(valid_normal) < 0.05; } if (cut_center) { if (!is_looking_forward) continue; } if (m_cut_parts[id].is_modifier) { glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); } GLGizmoBase::render_glmodel(m_cut_parts[id].glmodel, m_cut_parts[id].is_modifier ? MODIFIER_COLOR.get_data() : (m_cut_parts[id].is_up_part ? UPPER_PART_COLOR.get_data() : LOWER_PART_COLOR.get_data()), view_matrix * m_cut_parts[id].trans, projection_matrix); if (m_cut_parts[id].is_modifier) glsafe(::glDisable(GL_BLEND)); } } void 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); } } bool PartSelection::is_one_object() const { // In theory, the implementation could be just this: // return m_contour_to_parts.size() == m_ignored_contours.size(); // However, this would require that the part-contour correspondence works // flawlessly. Because it is currently not always so for self-intersecting // objects, let's better check the parts itself: if (m_cut_parts.size() < 2) return true; return std::all_of(m_cut_parts.begin(), m_cut_parts.end(), [this](const PartPara &part) { return part.is_modifier || part.is_up_part == m_cut_parts.front().is_up_part; }); } std::vector PartSelection::get_cut_parts() { std::vector parts; for (const auto &part : m_cut_parts) parts.push_back({part.is_up_part, part.is_modifier}); return parts; } bool PartSelection::has_modified_cut_parts() { if (m_back_cut_parts_state.size() == 0 || m_back_cut_parts_state.size() != m_cut_parts.size()) { return false; } for (size_t i = 0; i < m_cut_parts.size(); i++) { if (m_back_cut_parts_state[i] != m_cut_parts[i].is_up_part) { return true; } } return false; } // Read-only hit test shared by hover tooltip and right-click part assignment. // Returns the cut-part index under the mouse, or -1 when nothing is hit. int PartSelection::pick_part_id(const Vec2d &mouse_pos) const { if (!valid()) return -1; 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; id < m_cut_parts.size(); ++id) { const Transform3d &tr = m_cut_parts[id].trans; if (m_cut_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()) return -1; return int(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); } // Hover-only query for tooltip; must not call toggle_selection() which mutates part side. bool PartSelection::is_mouse_over_part(const Vec2d &mouse_pos) const { return valid() && !is_one_object() && pick_part_id(mouse_pos) >= 0; } void PartSelection::toggle_selection(const Vec2d &mouse_pos) { // Reuse the same raycast as hover detection, then flip the picked part side. const int id = pick_part_id(mouse_pos); if (id >= 0) toggle_selection(id); } void PartSelection::toggle_selection(int id) { if (id >= 0) { m_cut_parts[id].is_up_part = !m_cut_parts[id].is_up_part; // 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_cut_parts[upper].is_up_part; if (std::find_if(parts_below.begin(), parts_below.end(), [this, &upper_sel](const size_t &i) { return m_cut_parts[i].is_up_part == upper_sel; }) != parts_below.end()) { m_ignored_contours.emplace_back(cont_id); break; } } ++cont_id; }*/ } } void PartSelection::turn_over_selection() { for (PartPara &part : m_cut_parts) part.is_up_part = !part.is_up_part; } }} // namespace Slic3r