#include "GLGizmoPainterBase.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp" #include #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Camera.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/OpenGLManager.hpp" #include "slic3r/Utils/UndoRedo.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/TriangleMesh.hpp" #include #include namespace Slic3r::GUI { std::shared_ptr GLGizmoPainterBase::s_sphere = nullptr; GLGizmoPainterBase::GLGizmoPainterBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) : GLGizmoBase(parent, icon_filename, sprite_id) { } GLGizmoPainterBase::~GLGizmoPainterBase() { if (s_sphere != nullptr) s_sphere.reset(); } void GLGizmoPainterBase::data_changed(bool is_serializing) { if (m_state != On) return; const ModelObject* mo = m_c->selection_info() ? m_c->selection_info()->model_object() : nullptr; const Selection & selection = m_parent.get_selection(); if (mo && selection.is_from_single_instance() && (m_schedule_update || mo->id() != m_old_mo_id || mo->volumes.size() != m_old_volumes_size)) { update_from_model_object(); m_old_mo_id = mo->id(); m_old_volumes_size = mo->volumes.size(); m_schedule_update = false; } } GLGizmoPainterBase::ClippingPlaneDataWrapper GLGizmoPainterBase::get_clipping_plane_data() const { ClippingPlaneDataWrapper clp_data_out{{0.f, 0.f, 1.f, FLT_MAX}, {-FLT_MAX, FLT_MAX}}; // Take care of the clipping plane. The normal of the clipping plane is // saved with opposite sign than we need to pass to OpenGL (FIXME) if (bool clipping_plane_active = m_c->object_clipper()->get_position() != 0.; clipping_plane_active) { const ClippingPlane *clp = m_c->object_clipper()->get_clipping_plane(); for (size_t i = 0; i < 3; ++i) clp_data_out.clp_dataf[i] = -1.f * float(clp->get_data()[i]); clp_data_out.clp_dataf[3] = float(clp->get_data()[3]); } // z_range is calculated in the same way as in GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type) if (m_c->get_canvas()->get_use_clipping_planes()) { const std::array &clps = m_c->get_canvas()->get_clipping_planes(); clp_data_out.z_range = {float(-clps[0].get_data()[3]), float(clps[1].get_data()[3])}; } return clp_data_out; } void GLGizmoPainterBase::render_triangles(const Selection& selection) const { auto* shader = wxGetApp().get_shader("gouraud"); if (! shader) return; shader->start_using(); shader->set_uniform("slope.actived", false); shader->set_uniform("print_volume.type", 0); shader->set_uniform("clipping_plane", this->get_clipping_plane_data().clp_dataf); ScopeGuard guard([shader]() { if (shader) shader->stop_using(); }); const ModelObject *mo = m_c->selection_info()->model_object(); int mesh_id = -1; for (const ModelVolume* mv : mo->volumes) { if (! mv->is_model_part()) continue; ++mesh_id; const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); bool is_left_handed = trafo_matrix.matrix().determinant() < 0.; if (is_left_handed) glsafe(::glFrontFace(GL_CW)); const Camera& camera = wxGetApp().plater()->get_camera(); const Transform3d& view_matrix = camera.get_view_matrix(); shader->set_uniform("view_model_matrix", view_matrix * trafo_matrix); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); shader->set_uniform("view_normal_matrix", view_normal_matrix); // For printers with multiple extruders, it is necessary to pass trafo_matrix // to the shader input variable print_box.volume_world_matrix before // rendering the painted triangles. When this matrix is not set, the // wrong transformation matrix is used for "Clipping of view". shader->set_uniform("volume_world_matrix", trafo_matrix); m_triangle_selectors[mesh_id]->render(m_imgui, trafo_matrix); if (is_left_handed) glsafe(::glFrontFace(GL_CCW)); } } void GLGizmoPainterBase::render_cursor() { // First check that the mouse pointer is on an object. const ModelObject* mo = m_c->selection_info()->model_object(); const Selection& selection = m_parent.get_selection(); const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; const Camera& camera = wxGetApp().plater()->get_camera(); // Precalculate transformations of individual meshes. std::vector trafo_matrices; for (const ModelVolume* mv : mo->volumes) { if (mv->is_model_part()) trafo_matrices.emplace_back(mi->get_transformation().get_matrix() * mv->get_matrix()); } // Raycast and return if there's no hit. update_raycast_cache(m_parent.get_local_mouse_position(), camera, trafo_matrices); if (m_rr.mesh_id == -1) return; if (m_tool_type == ToolType::BRUSH) { if (m_cursor_type == TriangleSelector::SPHERE) render_cursor_sphere(trafo_matrices[m_rr.mesh_id]); else if (m_cursor_type == TriangleSelector::CIRCLE) render_cursor_circle(); } } void GLGizmoPainterBase::render_cursor_circle() { const Size cnv_size = m_parent.get_canvas_size(); const float cnv_width = float(cnv_size.get_width()); const float cnv_height = float(cnv_size.get_height()); if (cnv_width == 0.0f || cnv_height == 0.0f) return; const float cnv_inv_width = 1.0f / cnv_width; const float cnv_inv_height = 1.0f / cnv_height; const Vec2d center = m_parent.get_local_mouse_position(); #if ENABLE_GL_CORE_PROFILE || ENABLE_OPENGL_ES const float zoom = float(wxGetApp().plater()->get_camera().get_zoom()); const float radius = m_cursor_radius * zoom; #else const float radius = m_cursor_radius * float(wxGetApp().plater()->get_camera().get_zoom()); #endif // ENABLE_GL_CORE_PROFILE || ENABLE_OPENGL_ES #if ENABLE_GL_CORE_PROFILE if (!OpenGLManager::get_gl_info().is_core_profile()) #endif // ENABLE_GL_CORE_PROFILE glsafe(::glLineWidth(1.5f)); glsafe(::glDisable(GL_DEPTH_TEST)); #if !ENABLE_GL_CORE_PROFILE && !ENABLE_OPENGL_ES glsafe(::glPushAttrib(GL_ENABLE_BIT)); glsafe(::glLineStipple(4, 0xAAAA)); glsafe(::glEnable(GL_LINE_STIPPLE)); #endif // !ENABLE_GL_CORE_PROFILE && !ENABLE_OPENGL_ES #if ENABLE_GL_CORE_PROFILE || ENABLE_OPENGL_ES if (!m_circle.is_initialized() || std::abs(m_old_cursor_radius - radius) > EPSILON) { m_old_cursor_radius = radius; m_circle.reset(); #else if (!m_circle.is_initialized() || !m_old_center.isApprox(center) || std::abs(m_old_cursor_radius - radius) > EPSILON) { m_old_cursor_radius = radius; m_old_center = center; m_circle.reset(); #endif // ENABLE_GL_CORE_PROFILE || ENABLE_OPENGL_ES GLModel::Geometry init_data; #if ENABLE_GL_CORE_PROFILE || ENABLE_OPENGL_ES const unsigned int StepsCount = (unsigned int)(2 * (4 + int(252 * (zoom - 1.0f) / (250.0f - 1.0f)))); const float StepSize = 2.0f * float(PI) / float(StepsCount); init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P2 }; #else static const unsigned int StepsCount = 32; static const float StepSize = 2.0f * float(PI) / float(StepsCount); init_data.format = { GLModel::Geometry::EPrimitiveType::LineLoop, GLModel::Geometry::EVertexLayout::P2 }; #endif // ENABLE_GL_CORE_PROFILE || ENABLE_OPENGL_ES init_data.color = { 0.0f, 1.0f, 0.3f, 1.0f }; init_data.reserve_vertices(StepsCount); init_data.reserve_indices(StepsCount); // vertices + indices for (unsigned int i = 0; i < StepsCount; ++i) { #if ENABLE_GL_CORE_PROFILE || ENABLE_OPENGL_ES if (i % 2 != 0) continue; const float angle_i = float(i) * StepSize; const unsigned int j = (i + 1) % StepsCount; const float angle_j = float(j) * StepSize; const Vec2d v_i(::cos(angle_i), ::sin(angle_i)); const Vec2d v_j(::cos(angle_j), ::sin(angle_j)); init_data.add_vertex(Vec2f(v_i.x(), v_i.y())); init_data.add_vertex(Vec2f(v_j.x(), v_j.y())); const size_t vcount = init_data.vertices_count(); init_data.add_line(vcount - 2, vcount - 1); #else const float angle = float(i) * StepSize; init_data.add_vertex(Vec2f(2.0f * ((center.x() + ::cos(angle) * radius) * cnv_inv_width - 0.5f), -2.0f * ((center.y() + ::sin(angle) * radius) * cnv_inv_height - 0.5f))); init_data.add_index(i); #endif // ENABLE_GL_CORE_PROFILE || ENABLE_OPENGL_ES } m_circle.init_from(std::move(init_data)); } #if ENABLE_GL_CORE_PROFILE GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); #else GLShaderProgram* shader = GUI::wxGetApp().get_shader("flat"); #endif // ENABLE_GL_CORE_PROFILE if (shader != nullptr) { shader->start_using(); #if ENABLE_GL_CORE_PROFILE || ENABLE_OPENGL_ES const Transform3d view_model_matrix = Geometry::translation_transform(Vec3d(2.0f * (center.x() * cnv_inv_width - 0.5f), -2.0f * (center.y() * cnv_inv_height - 0.5f), 0.0)) * Geometry::scale_transform(Vec3d(2.0f * radius * cnv_inv_width, 2.0f * radius * cnv_inv_height, 1.0f)); shader->set_uniform("view_model_matrix", view_model_matrix); #else shader->set_uniform("view_model_matrix", Transform3d::Identity()); #endif // ENABLE_GL_CORE_PROFILE || ENABLE_OPENGL_ES shader->set_uniform("projection_matrix", Transform3d::Identity()); #if ENABLE_GL_CORE_PROFILE const std::array& viewport = wxGetApp().plater()->get_camera().get_viewport(); shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); shader->set_uniform("width", 0.25f); shader->set_uniform("gap_size", 0.0f); #endif // ENABLE_GL_CORE_PROFILE m_circle.render(); shader->stop_using(); } #if !ENABLE_GL_CORE_PROFILE && !ENABLE_OPENGL_ES glsafe(::glPopAttrib()); #endif // !ENABLE_GL_CORE_PROFILE && !ENABLE_OPENGL_ES glsafe(::glEnable(GL_DEPTH_TEST)); } void GLGizmoPainterBase::render_cursor_sphere(const Transform3d& trafo) const { if (s_sphere == nullptr) { s_sphere = std::make_shared(); s_sphere->init_from(its_make_sphere(1.0, double(PI) / 12.0)); } GLShaderProgram* shader = wxGetApp().get_shader("flat"); if (shader == nullptr) return; const Transform3d complete_scaling_matrix_inverse = Geometry::Transformation(trafo).get_scaling_factor_matrix().inverse(); ColorRGBA render_color = { 0.0f, 0.0f, 0.0f, 0.25f }; if (m_button_down == Button::Left) render_color = this->get_cursor_sphere_left_button_color(); else if (m_button_down == Button::Right) render_color = this->get_cursor_sphere_right_button_color(); shader->start_using(); const Camera& camera = wxGetApp().plater()->get_camera(); Transform3d view_model_matrix = camera.get_view_matrix() * trafo * Geometry::translation_transform(m_rr.hit.cast()) * complete_scaling_matrix_inverse * Geometry::scale_transform(m_cursor_radius * Vec3d::Ones()); shader->set_uniform("view_model_matrix", view_model_matrix); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); const bool is_left_handed = Geometry::Transformation(view_model_matrix).is_left_handed(); if (is_left_handed) glsafe(::glFrontFace(GL_CW)); assert(s_sphere != nullptr); s_sphere->set_color(render_color); s_sphere->render(); if (is_left_handed) glsafe(::glFrontFace(GL_CCW)); shader->stop_using(); } bool GLGizmoPainterBase::is_mesh_point_clipped(const Vec3d& point, const Transform3d& trafo) const { if (m_c->object_clipper()->get_position() == 0.) return false; auto sel_info = m_c->selection_info(); Vec3d transformed_point = trafo * point; transformed_point(2) += sel_info->get_sla_shift(); return m_c->object_clipper()->get_clipping_plane()->is_point_clipped(transformed_point); } // Interpolate points between the previous and current mouse positions, which are then projected onto the object. // Returned projected mouse positions are grouped by mesh_idx. It may contain multiple std::vector // with the same mesh_idx, but all items in std::vector always have the same mesh_idx. std::vector> GLGizmoPainterBase::get_projected_mouse_positions(const Vec2d &mouse_position, const double resolution, const std::vector &trafo_matrices) const { // List of mouse positions that will be used as seeds for painting. std::vector mouse_positions{mouse_position}; if (m_last_mouse_click != Vec2d::Zero()) { // In case current mouse position is far from the last one, // add several positions from between into the list, so there // are no gaps in the painted region. if (size_t patches_in_between = size_t((mouse_position - m_last_mouse_click).norm() / resolution); patches_in_between > 0) { const Vec2d diff = (m_last_mouse_click - mouse_position) / (patches_in_between + 1); for (size_t patch_idx = 1; patch_idx <= patches_in_between; ++patch_idx) mouse_positions.emplace_back(mouse_position + patch_idx * diff); mouse_positions.emplace_back(m_last_mouse_click); } } const Camera &camera = wxGetApp().plater()->get_camera(); std::vector mesh_hit_points; mesh_hit_points.reserve(mouse_positions.size()); // In mesh_hit_points only the last item could have mesh_id == -1, any other items mustn't. for (const Vec2d &mp : mouse_positions) { update_raycast_cache(mp, camera, trafo_matrices); mesh_hit_points.push_back({m_rr.hit, m_rr.mesh_id, m_rr.facet}); if (m_rr.mesh_id == -1) break; } // Divide mesh_hit_points into groups with the same mesh_idx. It may contain multiple groups with the same mesh_idx. std::vector> mesh_hit_points_by_mesh; for (size_t prev_mesh_hit_point = 0, curr_mesh_hit_point = 0; curr_mesh_hit_point < mesh_hit_points.size(); ++curr_mesh_hit_point) { size_t next_mesh_hit_point = curr_mesh_hit_point + 1; if (next_mesh_hit_point >= mesh_hit_points.size() || mesh_hit_points[curr_mesh_hit_point].mesh_idx != mesh_hit_points[next_mesh_hit_point].mesh_idx) { mesh_hit_points_by_mesh.emplace_back(); mesh_hit_points_by_mesh.back().insert(mesh_hit_points_by_mesh.back().end(), mesh_hit_points.begin() + int(prev_mesh_hit_point), mesh_hit_points.begin() + int(next_mesh_hit_point)); prev_mesh_hit_point = next_mesh_hit_point; } } auto on_same_facet = [](std::vector &hit_points) -> bool { for (const ProjectedMousePosition &mesh_hit_point : hit_points) if (mesh_hit_point.facet_idx != hit_points.front().facet_idx) return false; return true; }; struct Plane { Vec3d origin; Vec3d first_axis; Vec3d second_axis; }; auto find_plane = [](std::vector &hit_points) -> std::optional { assert(hit_points.size() >= 3); for (size_t third_idx = 2; third_idx < hit_points.size(); ++third_idx) { const Vec3d &first_point = hit_points[third_idx - 2].mesh_hit.cast(); const Vec3d &second_point = hit_points[third_idx - 1].mesh_hit.cast(); const Vec3d &third_point = hit_points[third_idx].mesh_hit.cast(); const Vec3d first_vec = first_point - second_point; const Vec3d second_vec = third_point - second_point; // If three points aren't collinear, then there exists only one plane going through all points. if (first_vec.cross(second_vec).squaredNorm() > sqr(EPSILON)) { const Vec3d first_axis_vec_n = first_vec.normalized(); // Make second_vec perpendicular to first_axis_vec_n using Gram–Schmidt orthogonalization process const Vec3d second_axis_vec_n = (second_vec - (first_vec.dot(second_vec) / first_vec.dot(first_vec)) * first_vec).normalized(); return Plane{second_point, first_axis_vec_n, second_axis_vec_n}; } } return std::nullopt; }; for(std::vector &hit_points : mesh_hit_points_by_mesh) { assert(!hit_points.empty()); if (hit_points.back().mesh_idx == -1) break; if (hit_points.size() <= 2) continue; if (on_same_facet(hit_points)) { hit_points = {hit_points.front(), hit_points.back()}; } else if (std::optional plane = find_plane(hit_points); plane) { Polyline polyline; polyline.points.reserve(hit_points.size()); // Project hit_points into its plane to simplified them in the next step. for (auto &hit_point : hit_points) { const Vec3d &point = hit_point.mesh_hit.cast(); const double x_cord = plane->first_axis.dot(point - plane->origin); const double y_cord = plane->second_axis.dot(point - plane->origin); polyline.points.emplace_back(scale_(x_cord), scale_(y_cord)); } polyline.simplify(scale_(m_cursor_radius) / 10.); const int mesh_idx = hit_points.front().mesh_idx; std::vector new_hit_points; new_hit_points.reserve(polyline.points.size()); // Project 2D simplified hit_points beck to 3D. for (const Point &point : polyline.points) { const double x_cord = unscale(point.x()); const double y_cord = unscale(point.y()); const Vec3d new_hit_point = plane->origin + x_cord * plane->first_axis + y_cord * plane->second_axis; const int facet_idx = m_c->raycaster()->raycasters()[mesh_idx]->get_closest_facet(new_hit_point.cast()); new_hit_points.push_back({new_hit_point.cast(), mesh_idx, size_t(facet_idx)}); } hit_points = new_hit_points; } else { hit_points = {hit_points.front(), hit_points.back()}; } } return mesh_hit_points_by_mesh; } // Following function is called from GLCanvas3D to inform the gizmo about a mouse/keyboard event. // The gizmo has an opportunity to react - if it does, it should return true so that the Canvas3D is // aware that the event was reacted to and stops trying to make different sense of it. If the gizmo // concludes that the event was not intended for it, it should return false. bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) { if (action == SLAGizmoEventType::MouseWheelUp || action == SLAGizmoEventType::MouseWheelDown) { if (control_down) { double pos = m_c->object_clipper()->get_position(); pos = action == SLAGizmoEventType::MouseWheelDown ? std::max(0., pos - 0.01) : std::min(1., pos + 0.01); m_c->object_clipper()->set_position_by_ratio(pos, true); return true; } else if (alt_down) { if (m_tool_type == ToolType::BRUSH && (m_cursor_type == TriangleSelector::CursorType::SPHERE || m_cursor_type == TriangleSelector::CursorType::CIRCLE)) { m_cursor_radius = action == SLAGizmoEventType::MouseWheelDown ? std::max(m_cursor_radius - this->get_cursor_radius_step(), this->get_cursor_radius_min()) : std::min(m_cursor_radius + this->get_cursor_radius_step(), this->get_cursor_radius_max()); m_parent.set_as_dirty(); return true; } else if (m_tool_type == ToolType::SMART_FILL) { m_smart_fill_angle = action == SLAGizmoEventType::MouseWheelDown ? std::max(m_smart_fill_angle - SmartFillAngleStep, SmartFillAngleMin) : std::min(m_smart_fill_angle + SmartFillAngleStep, SmartFillAngleMax); m_parent.set_as_dirty(); if (m_rr.mesh_id != -1) { const Selection &selection = m_parent.get_selection(); const ModelObject *mo = m_c->selection_info()->model_object(); const ModelInstance *mi = mo->instances[selection.get_instance_idx()]; const Transform3d trafo_matrix_not_translate = mi->get_transformation().get_matrix_no_offset() * mo->volumes[m_rr.mesh_id]->get_matrix_no_offset(); const Transform3d trafo_matrix = mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, this->get_clipping_plane_in_volume_coordinates(trafo_matrix), m_smart_fill_angle, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } return true; } return false; } } if (action == SLAGizmoEventType::ResetClippingPlane) { m_c->object_clipper()->set_position_by_ratio(-1., false); return true; } if (action == SLAGizmoEventType::LeftDown || action == SLAGizmoEventType::RightDown || (action == SLAGizmoEventType::Dragging && m_button_down != Button::None)) { if (m_triangle_selectors.empty()) return false; EnforcerBlockerType new_state = EnforcerBlockerType::NONE; if (! shift_down) { if (action == SLAGizmoEventType::Dragging) new_state = m_button_down == Button::Left ? this->get_left_button_state_type() : this->get_right_button_state_type(); else new_state = action == SLAGizmoEventType::LeftDown ? this->get_left_button_state_type() : this->get_right_button_state_type(); } const Camera &camera = wxGetApp().plater()->get_camera(); const Selection &selection = m_parent.get_selection(); const ModelObject *mo = m_c->selection_info()->model_object(); const ModelInstance *mi = mo->instances[selection.get_instance_idx()]; const Transform3d instance_trafo = mi->get_transformation().get_matrix(); const Transform3d instance_trafo_not_translate = mi->get_transformation().get_matrix_no_offset(); // Precalculate transformations of individual meshes. std::vector trafo_matrices; std::vector trafo_matrices_not_translate; for (const ModelVolume *mv : mo->volumes) if (mv->is_model_part()) { trafo_matrices.emplace_back(instance_trafo * mv->get_matrix()); trafo_matrices_not_translate.emplace_back(instance_trafo_not_translate * mv->get_matrix_no_offset()); } std::vector> projected_mouse_positions_by_mesh = get_projected_mouse_positions(mouse_position, 1., trafo_matrices); m_last_mouse_click = Vec2d::Zero(); // only actual hits should be saved for (const std::vector &projected_mouse_positions : projected_mouse_positions_by_mesh) { assert(!projected_mouse_positions.empty()); const int mesh_idx = projected_mouse_positions.front().mesh_idx; const bool dragging_while_painting = (action == SLAGizmoEventType::Dragging && m_button_down != Button::None); // The mouse button click detection is enabled when there is a valid hit. // Missing the object entirely // shall not capture the mouse. if (mesh_idx != -1) if (m_button_down == Button::None) m_button_down = ((action == SLAGizmoEventType::LeftDown) ? Button::Left : Button::Right); // In case we have no valid hit, we can return. The event will be stopped when // dragging while painting (to prevent scene rotations and moving the object) if (mesh_idx == -1) return dragging_while_painting; const Transform3d &trafo_matrix = trafo_matrices[mesh_idx]; const Transform3d &trafo_matrix_not_translate = trafo_matrices_not_translate[mesh_idx]; // Calculate direction from camera to the hit (in mesh coords): Vec3f camera_pos = (trafo_matrix.inverse() * camera.get_position()).cast(); assert(mesh_idx < int(m_triangle_selectors.size())); const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); if (m_tool_type == ToolType::SMART_FILL || m_tool_type == ToolType::BUCKET_FILL || (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER)) { for(const ProjectedMousePosition &projected_mouse_position : projected_mouse_positions) { assert(projected_mouse_position.mesh_idx == mesh_idx); const Vec3f mesh_hit = projected_mouse_position.mesh_hit; const int facet_idx = int(projected_mouse_position.facet_idx); m_triangle_selectors[mesh_idx]->seed_fill_apply_on_triangles(new_state); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, false, true); else if (m_tool_type == ToolType::BUCKET_FILL) m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, true, true); m_seed_fill_last_mesh_id = -1; } } else if (m_tool_type == ToolType::BRUSH) { assert(m_cursor_type == TriangleSelector::CursorType::CIRCLE || m_cursor_type == TriangleSelector::CursorType::SPHERE); if (projected_mouse_positions.size() == 1) { const ProjectedMousePosition &first_position = projected_mouse_positions.front(); std::unique_ptr cursor = TriangleSelector::SinglePointCursor::cursor_factory(first_position.mesh_hit, camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); m_triangle_selectors[mesh_idx]->select_patch(int(first_position.facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); } else { for (auto first_position_it = projected_mouse_positions.cbegin(); first_position_it != projected_mouse_positions.cend() - 1; ++first_position_it) { auto second_position_it = first_position_it + 1; std::unique_ptr cursor = TriangleSelector::DoublePointCursor::cursor_factory(first_position_it->mesh_hit, second_position_it->mesh_hit, camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); m_triangle_selectors[mesh_idx]->select_patch(int(first_position_it->facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); } } } m_triangle_selectors[mesh_idx]->request_update_render_data(); m_last_mouse_click = mouse_position; } return true; } if (action == SLAGizmoEventType::Moving && (m_tool_type == ToolType::SMART_FILL || m_tool_type == ToolType::BUCKET_FILL || (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER))) { if (m_triangle_selectors.empty()) return false; const Camera &camera = wxGetApp().plater()->get_camera(); const Selection &selection = m_parent.get_selection(); const ModelObject *mo = m_c->selection_info()->model_object(); const ModelInstance *mi = mo->instances[selection.get_instance_idx()]; const Transform3d instance_trafo = mi->get_transformation().get_matrix(); const Transform3d instance_trafo_not_translate = mi->get_transformation().get_matrix_no_offset(); // Precalculate transformations of individual meshes. std::vector trafo_matrices; std::vector trafo_matrices_not_translate; for (const ModelVolume *mv : mo->volumes) if (mv->is_model_part()) { trafo_matrices.emplace_back(instance_trafo * mv->get_matrix()); trafo_matrices_not_translate.emplace_back(instance_trafo_not_translate* mv->get_matrix_no_offset()); } // Now "click" into all the prepared points and spill paint around them. update_raycast_cache(mouse_position, camera, trafo_matrices); auto seed_fill_unselect_all = [this]() { for (auto &triangle_selector : m_triangle_selectors) { triangle_selector->seed_fill_unselect_all_triangles(); triangle_selector->request_update_render_data(); } }; if (m_rr.mesh_id == -1) { // Clean selected by seed fill for all triangles in all meshes when a mouse isn't pointing on any mesh. seed_fill_unselect_all(); m_seed_fill_last_mesh_id = -1; // In case we have no valid hit, we can return. return false; } // The mouse moved from one object's volume to another one. So it is needed to unselect all triangles selected by seed fill. if(m_rr.mesh_id != m_seed_fill_last_mesh_id) seed_fill_unselect_all(); const Transform3d &trafo_matrix = trafo_matrices[m_rr.mesh_id]; const Transform3d &trafo_matrix_not_translate = trafo_matrices_not_translate[m_rr.mesh_id]; assert(m_rr.mesh_id < int(m_triangle_selectors.size())); const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, false); else if (m_tool_type == ToolType::BUCKET_FILL) m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, true); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; return true; } if ((action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::RightUp) && m_button_down != Button::None) { // Take snapshot and update ModelVolume data. wxString action_name = this->handle_snapshot_action_name(shift_down, m_button_down); Plater::TakeSnapshot snapshot(wxGetApp().plater(), action_name, UndoRedo::SnapshotType::GizmoAction); update_model_object(); m_button_down = Button::None; m_last_mouse_click = Vec2d::Zero(); return true; } return false; } bool GLGizmoPainterBase::on_mouse(const wxMouseEvent &mouse_event) { // wxCoord == int --> wx/types.h Vec2i mouse_coord(mouse_event.GetX(), mouse_event.GetY()); Vec2d mouse_pos = mouse_coord.cast(); if (mouse_event.Moving()) { gizmo_event(SLAGizmoEventType::Moving, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false); return false; } // when control is down we allow scene pan and rotation even when clicking // over some object bool control_down = mouse_event.CmdDown(); bool grabber_contains_mouse = (get_hover_id() != -1); const Selection &selection = m_parent.get_selection(); int selected_object_idx = selection.get_object_idx(); if (mouse_event.LeftDown()) { if ((!control_down || grabber_contains_mouse) && gizmo_event(SLAGizmoEventType::LeftDown, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false)) // the gizmo got the event and took some action, there is no need // to do anything more return true; } else if (mouse_event.RightDown()){ if (!control_down && selected_object_idx != -1 && gizmo_event(SLAGizmoEventType::RightDown, mouse_pos, false, false, false)) // event was taken care of return true; } else if (mouse_event.Dragging()) { if (m_parent.get_move_volume_id() != -1) // don't allow dragging objects with the Sla gizmo on return true; if (!control_down && gizmo_event(SLAGizmoEventType::Dragging, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false)) { // the gizmo got the event and took some action, no need to do // anything more here m_parent.set_as_dirty(); return true; } if(control_down && (mouse_event.LeftIsDown() || mouse_event.RightIsDown())) { // CTRL has been pressed while already dragging -> stop current action if (mouse_event.LeftIsDown()) gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), true); else if (mouse_event.RightIsDown()) gizmo_event(SLAGizmoEventType::RightUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), true); return false; } } else if (mouse_event.LeftUp()) { if (!m_parent.is_mouse_dragging()) { // in case SLA/FDM gizmo is selected, we just pass the LeftUp // event and stop processing - neither object moving or selecting // is suppressed in that case gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), control_down); return true; } } else if (mouse_event.RightUp()) { if (!m_parent.is_mouse_dragging()) { gizmo_event(SLAGizmoEventType::RightUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), control_down); return true; } } return false; } void GLGizmoPainterBase::update_raycast_cache(const Vec2d& mouse_position, const Camera& camera, const std::vector& trafo_matrices) const { if (m_rr.mouse_position == mouse_position) { // Same query as last time - the answer is already in the cache. return; } Vec3f normal = Vec3f::Zero(); Vec3f hit = Vec3f::Zero(); size_t facet = 0; Vec3f closest_hit = Vec3f::Zero(); double closest_hit_squared_distance = std::numeric_limits::max(); size_t closest_facet = 0; int closest_hit_mesh_id = -1; // Cast a ray on all meshes, pick the closest hit and save it for the respective mesh for (int mesh_id = 0; mesh_id < int(trafo_matrices.size()); ++mesh_id) { if (m_c->raycaster()->raycasters()[mesh_id]->unproject_on_mesh( mouse_position, trafo_matrices[mesh_id], camera, hit, normal, m_c->object_clipper()->get_clipping_plane(), &facet)) { // In case this hit is clipped, skip it. if (is_mesh_point_clipped(hit.cast(), trafo_matrices[mesh_id])) continue; // Is this hit the closest to the camera so far? double hit_squared_distance = (camera.get_position()-trafo_matrices[mesh_id]*hit.cast()).squaredNorm(); if (hit_squared_distance < closest_hit_squared_distance) { closest_hit_squared_distance = hit_squared_distance; closest_facet = facet; closest_hit_mesh_id = mesh_id; closest_hit = hit; } } } m_rr = {mouse_position, closest_hit_mesh_id, closest_hit, closest_facet}; } bool GLGizmoPainterBase::on_is_activable() const { const Selection& selection = m_parent.get_selection(); if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptFFF || !selection.is_single_full_instance() || wxGetApp().get_mode() == comSimple) return false; // Check that none of the selected volumes is outside. Only SLA auxiliaries (supports) are allowed outside. const Selection::IndicesList& list = selection.get_volume_idxs(); return std::all_of(list.cbegin(), list.cend(), [&selection](unsigned int idx) { return !selection.get_volume(idx)->is_outside; }); } bool GLGizmoPainterBase::on_is_selectable() const { return (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF && wxGetApp().get_mode() != comSimple ); } CommonGizmosDataID GLGizmoPainterBase::on_get_requirements() const { return CommonGizmosDataID( int(CommonGizmosDataID::SelectionInfo) | int(CommonGizmosDataID::InstancesHider) | int(CommonGizmosDataID::Raycaster) | int(CommonGizmosDataID::ObjectClipper)); } void GLGizmoPainterBase::on_set_state() { if (m_state == m_old_state) return; if (m_state == On && m_old_state != On) { // the gizmo was just turned on on_opening(); } if (m_state == Off && m_old_state != Off) { // the gizmo was just turned Off // we are actually shutting down on_shutdown(); m_old_mo_id = -1; //m_iva.release_geometry(); m_triangle_selectors.clear(); } m_old_state = m_state; } void GLGizmoPainterBase::on_load(cereal::BinaryInputArchive&) { // We should update the gizmo from current ModelObject, but it is not // possible at this point. That would require having updated selection and // common gizmos data, which is not done at this point. Instead, save // a flag to do the update in set_painter_gizmo_data, which will be called // soon after. m_schedule_update = true; } TriangleSelector::ClippingPlane GLGizmoPainterBase::get_clipping_plane_in_volume_coordinates(const Transform3d &trafo) const { const ::Slic3r::GUI::ClippingPlane *const clipping_plane = m_c->object_clipper()->get_clipping_plane(); if (clipping_plane == nullptr || !clipping_plane->is_active()) return {}; const Vec3d clp_normal = clipping_plane->get_normal(); const double clp_offset = clipping_plane->get_offset(); const Transform3d trafo_normal = Transform3d(trafo.linear().transpose()); const Transform3d trafo_inv = trafo.inverse(); Vec3d point_on_plane = clp_normal * clp_offset; Vec3d point_on_plane_transformed = trafo_inv * point_on_plane; Vec3d normal_transformed = trafo_normal * clp_normal; auto offset_transformed = float(point_on_plane_transformed.dot(normal_transformed)); return TriangleSelector::ClippingPlane({float(normal_transformed.x()), float(normal_transformed.y()), float(normal_transformed.z()), offset_transformed}); } ColorRGBA TriangleSelectorGUI::get_seed_fill_color(const ColorRGBA& base_color) { return saturate(base_color, 0.75f); } void TriangleSelectorGUI::render(ImGuiWrapper* imgui, const Transform3d& matrix) { static const ColorRGBA enforcers_color = { 0.47f, 0.47f, 1.0f, 1.0f }; static const ColorRGBA blockers_color = { 1.0f, 0.44f, 0.44f, 1.0f }; if (m_update_render_data) { update_render_data(); m_update_render_data = false; } auto* shader = wxGetApp().get_current_shader(); if (! shader) return; assert(shader->get_name() == "gouraud"); for (auto iva : {std::make_pair(&m_iva_enforcers, enforcers_color), std::make_pair(&m_iva_blockers, blockers_color)}) { iva.first->set_color(iva.second); iva.first->render(); } for (auto& iva : m_iva_seed_fills) { size_t color_idx = &iva - &m_iva_seed_fills.front(); const ColorRGBA& color = TriangleSelectorGUI::get_seed_fill_color(color_idx == 1 ? enforcers_color : color_idx == 2 ? blockers_color : GLVolume::NEUTRAL_COLOR); iva.set_color(color); iva.render(); } render_paint_contour(matrix); #ifdef QIDISLICER_TRIANGLE_SELECTOR_DEBUG if (imgui) render_debug(imgui); else assert(false); // If you want debug output, pass ptr to ImGuiWrapper. #endif // QIDISLICER_TRIANGLE_SELECTOR_DEBUG } void TriangleSelectorGUI::update_render_data() { int enf_cnt = 0; int blc_cnt = 0; std::vector seed_fill_cnt(m_iva_seed_fills.size(), 0); for (auto* iva : { &m_iva_enforcers, &m_iva_blockers }) { iva->reset(); } for (auto& iva : m_iva_seed_fills) { iva.reset(); } GLModel::Geometry iva_enforcers_data; iva_enforcers_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; GLModel::Geometry iva_blockers_data; iva_blockers_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; std::array iva_seed_fills_data; for (auto& data : iva_seed_fills_data) data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; // small value used to offset triangles along their normal to avoid z-fighting static const float offset = 0.001f; for (const Triangle &tr : m_triangles) { if (!tr.valid() || tr.is_split() || (tr.get_state() == EnforcerBlockerType::NONE && !tr.is_selected_by_seed_fill())) continue; int tr_state = int(tr.get_state()); GLModel::Geometry &iva = tr.is_selected_by_seed_fill() ? iva_seed_fills_data[tr_state] : tr.get_state() == EnforcerBlockerType::ENFORCER ? iva_enforcers_data : iva_blockers_data; int &cnt = tr.is_selected_by_seed_fill() ? seed_fill_cnt[tr_state] : tr.get_state() == EnforcerBlockerType::ENFORCER ? enf_cnt : blc_cnt; const Vec3f &v0 = m_vertices[tr.verts_idxs[0]].v; const Vec3f &v1 = m_vertices[tr.verts_idxs[1]].v; const Vec3f &v2 = m_vertices[tr.verts_idxs[2]].v; //FIXME the normal may likely be pulled from m_triangle_selectors, but it may not be worth the effort // or the current implementation may be more cache friendly. const Vec3f n = (v1 - v0).cross(v2 - v1).normalized(); // small value used to offset triangles along their normal to avoid z-fighting const Vec3f offset_n = offset * n; iva.add_vertex(v0 + offset_n, n); iva.add_vertex(v1 + offset_n, n); iva.add_vertex(v2 + offset_n, n); iva.add_triangle((unsigned int)cnt, (unsigned int)cnt + 1, (unsigned int)cnt + 2); cnt += 3; } if (!iva_enforcers_data.is_empty()) m_iva_enforcers.init_from(std::move(iva_enforcers_data)); if (!iva_blockers_data.is_empty()) m_iva_blockers.init_from(std::move(iva_blockers_data)); for (size_t i = 0; i < m_iva_seed_fills.size(); ++i) { if (!iva_seed_fills_data[i].is_empty()) m_iva_seed_fills[i].init_from(std::move(iva_seed_fills_data[i])); } update_paint_contour(); } #ifdef QIDISLICER_TRIANGLE_SELECTOR_DEBUG void TriangleSelectorGUI::render_debug(ImGuiWrapper* imgui) { imgui->begin(std::string("TriangleSelector dialog (DEV ONLY)"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); static float edge_limit = 1.f; imgui->text("Edge limit (mm): "); imgui->slider_float("", &edge_limit, 0.1f, 8.f); set_edge_limit(edge_limit); imgui->checkbox("Show split triangles: ", m_show_triangles); imgui->checkbox("Show invalid triangles: ", m_show_invalid); int valid_triangles = m_triangles.size() - m_invalid_triangles; imgui->text("Valid triangles: " + std::to_string(valid_triangles) + "/" + std::to_string(m_triangles.size())); imgui->text("Vertices: " + std::to_string(m_vertices.size())); if (imgui->button("Force garbage collection")) garbage_collect(); if (imgui->button("Serialize - deserialize")) { auto map = serialize(); deserialize(map); } imgui->end(); if (! m_show_triangles) return; enum vtype { ORIGINAL = 0, SPLIT, INVALID }; for (auto& va : m_varrays) va.reset(); std::array cnts; ::glScalef(1.01f, 1.01f, 1.01f); std::array varrays_data; for (auto& data : varrays_data) data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3, GLModel::Geometry::EIndexType::UINT }; for (int tr_id=0; tr_idadd_vertex(m_vertices[tr.verts_idxs[i]].v, Vec3f(0.0f, 0.0f, 1.0f)); } va->add_uint_triangle((unsigned int)*cnt, (unsigned int)*cnt + 1, (unsigned int)*cnt + 2); *cnt += 3; } for (int i = 0; i < 3; ++i) { if (!varrays_data[i].is_empty()) m_varrays[i].init_from(std::move(varrays_data[i])); } GLShaderProgram* curr_shader = wxGetApp().get_current_shader(); if (curr_shader != nullptr) curr_shader->stop_using(); GLShaderProgram* shader = wxGetApp().get_shader("flat"); if (shader != nullptr) { shader->start_using(); const Camera& camera = wxGetApp().plater()->get_camera(); shader->set_uniform("view_model_matrix", camera.get_view_matrix()); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); ::glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); for (vtype i : {ORIGINAL, SPLIT, INVALID}) { GLModel& va = m_varrays[i]; switch (i) { case ORIGINAL: va.set_color({ 0.0f, 0.0f, 1.0f, 1.0f }); break; case SPLIT: va.set_color({ 1.0f, 0.0f, 0.0f, 1.0f }); break; case INVALID: va.set_color({ 1.0f, 1.0f, 0.0f, 1.0f }); break; } va.render(); } ::glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); shader->stop_using(); } if (curr_shader != nullptr) curr_shader->start_using(); } #endif // QIDISLICER_TRIANGLE_SELECTOR_DEBUG void TriangleSelectorGUI::update_paint_contour() { m_paint_contour.reset(); GLModel::Geometry init_data; const std::vector contour_edges = this->get_seed_fill_contour(); init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; init_data.reserve_vertices(2 * contour_edges.size()); init_data.reserve_indices(2 * contour_edges.size()); init_data.color = ColorRGBA::WHITE(); // vertices + indices unsigned int vertices_count = 0; for (const Vec2i& edge : contour_edges) { init_data.add_vertex(m_vertices[edge(0)].v); init_data.add_vertex(m_vertices[edge(1)].v); vertices_count += 2; init_data.add_line(vertices_count - 2, vertices_count - 1); } if (!init_data.is_empty()) m_paint_contour.init_from(std::move(init_data)); } void TriangleSelectorGUI::render_paint_contour(const Transform3d& matrix) { auto* curr_shader = wxGetApp().get_current_shader(); if (curr_shader != nullptr) curr_shader->stop_using(); auto* contour_shader = wxGetApp().get_shader("mm_contour"); if (contour_shader != nullptr) { contour_shader->start_using(); contour_shader->set_uniform("offset", OpenGLManager::get_gl_info().is_mesa() ? 0.0005 : 0.00001); const Camera& camera = wxGetApp().plater()->get_camera(); contour_shader->set_uniform("view_model_matrix", camera.get_view_matrix() * matrix); contour_shader->set_uniform("projection_matrix", camera.get_projection_matrix()); m_paint_contour.render(); contour_shader->stop_using(); } if (curr_shader != nullptr) curr_shader->start_using(); } } // namespace Slic3r::GUI