// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. #include "GLGizmoSVG.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "libslic3r/AppConfig.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp" #include "slic3r/GUI/MainFrame.hpp" // to update title when add text #include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/GUI/MsgDialog.hpp" #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/CameraUtils.hpp" #include "slic3r/Utils/UndoRedo.hpp" #include "libslic3r/Point.hpp" #include "libslic3r/SVG.hpp" // debug store #include "libslic3r/Geometry.hpp" // covex hull 2d #include "libslic3r/Timer.hpp" // covex hull 2d #include "libslic3r/NSVGUtils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/ClipperUtils.hpp" // union_ex #include "imgui/imgui_stdlib.h" // using std::string for inputs #ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS #endif #include #include "nanosvg/nanosvg.h" // load SVG file #include // detection of change DPI #include #include #include #include #include // measure enumeration of fonts #include // save for svg #include #include #include "slic3r/GUI/BitmapCache.hpp" #include "libslic3r/AABBTreeLines.hpp" // aabb lines for draw filled expolygon #include #include // use surface cuts #include "slic3r/GUI/Jobs/Worker.hpp" using namespace Slic3r; using namespace Slic3r::Emboss; using namespace Slic3r::GUI; namespace Slic3r { namespace GUI { const int c_move_cube_id = 1; const std::array SVG_Move_GrabberColor = {1.0, 1.0, 0.0, 1.0}; const std::array SVG_Move_GrabberHoverColor = {0.7, 0.7, 0.0, 1.0}; // TRN - Title in Undo/Redo stack after rotate with SVG around emboss axe const std::string rotation_snapshot_name = "SVG rotate"; // NOTE: Translation is made in "m_parent.do_rotate()" // TRN - Title in Undo/Redo stack after move with SVG along emboss axe - From surface const std::string move_snapshot_name = "SVG move"; // NOTE: Translation is made in "m_parent.do_translate()" // Variable keep limits for variables const struct Limits { MinMax depth{0.01, 1e4}; // in mm MinMax size{0.01f, 1e4f}; // in mm (width + height) MinMax ui_size{5.f, 100.f}; // in mm (width + height) - only slider values MinMax ui_size_in{.1f, 4.f}; // in inches (width + height) - only slider values MinMax relative_scale_ratio{1e-5, 1e4}; // change size // distance text object from surface MinMax angle{-180.f, 180.f}; // in degrees } limits; wxString last_used_directory = wxEmptyString; std::string choose_svg_file() { wxWindow * parent = nullptr; wxString message = _L("Choose SVG file for emboss:"); wxString selected_file = wxEmptyString; wxString wildcard = file_wildcards(FT_SVG); long style = wxFD_OPEN | wxFD_FILE_MUST_EXIST; wxFileDialog dialog(parent, message, last_used_directory, selected_file, wildcard, style); if (dialog.ShowModal() != wxID_OK) { BOOST_LOG_TRIVIAL(warning) << "SVG file for emboss was NOT selected."; return {}; } wxArrayString input_files; dialog.GetPaths(input_files); if (input_files.IsEmpty()) { BOOST_LOG_TRIVIAL(warning) << "SVG file dialog result is empty."; return {}; } if (input_files.size() != 1) BOOST_LOG_TRIVIAL(warning) << "SVG file dialog result contain multiple files but only first is used."; std::string path = into_u8(input_files.front()); if (!boost::filesystem::exists(path)) { BOOST_LOG_TRIVIAL(warning) << "SVG file dialog return invalid path."; return {}; } if (!boost::algorithm::iends_with(path, ".svg")) { BOOST_LOG_TRIVIAL(warning) << "SVG file dialog return path without '.svg' tail"; return {}; } last_used_directory = dialog.GetDirectory(); return path; } double get_tesselation_tolerance(double scale) { double tesselation_tolerance_in_mm = .1; // 8e-2; double tesselation_tolerance_scaled = (tesselation_tolerance_in_mm * tesselation_tolerance_in_mm) / SCALING_FACTOR / SCALING_FACTOR; return tesselation_tolerance_scaled / scale / scale; } EmbossShape select_shape(std::string_view filepath, double tesselation_tolerance = get_tesselation_tolerance(1.)) { EmbossShape shape; shape.projection.depth = 10.; shape.projection.use_surface = false; EmbossShape::SvgFile svg; if (filepath.empty()) { // When empty open file dialog svg.path = choose_svg_file(); if (svg.path.empty()) return {}; // file was not selected } else { svg.path = filepath; // copy } boost::filesystem::path path(svg.path); if (!boost::filesystem::exists(path)) { show_error(nullptr, GUI::format(_u8L("File does NOT exist (%1%)."), svg.path)); return {}; } if (!boost::algorithm::iends_with(svg.path, ".svg")) { show_error(nullptr, GUI::format(_u8L("Filename has to end with \".svg\" but you selected %1%"), svg.path)); return {}; } if (init_image(svg) == nullptr) { show_error(nullptr, GUI::format(_u8L("Nano SVG parser can't load from file (%1%)."), svg.path)); return {}; } // Set default and unchanging scale NSVGLineParams params{tesselation_tolerance}; shape.shapes_with_ids = create_shape_with_ids(*svg.image, params); // Must contain some shapes !!! if (shape.shapes_with_ids.empty()) { show_error(nullptr, GUI::format(_u8L("%1% contains some unsupported data. Please use third-party software to convert the SVG to path data before reimporting."), svg.path)); return {}; } shape.svg_file = std::move(svg); return shape; } std::string get_file_name(const std::string &file_path) { if (file_path.empty()) return file_path; size_t pos_last_delimiter = file_path.find_last_of("/\\"); if (pos_last_delimiter == std::string::npos) { // should not happend that in path is not delimiter assert(false); pos_last_delimiter = 0; } size_t pos_point = file_path.find_last_of('.'); if (pos_point == std::string::npos || pos_point < pos_last_delimiter // last point is inside of directory path ) { // there is no extension assert(false); pos_point = file_path.size(); } size_t offset = pos_last_delimiter + 1; // result should not contain last delimiter ( +1 ) size_t count = pos_point - pos_last_delimiter - 1; // result should not contain extension point ( -1 ) return file_path.substr(offset, count); } std::string volume_name(const EmbossShape &shape) { std::string file_name = get_file_name(shape.svg_file->path); if (!file_name.empty()) return file_name; return "SVG shape"; } // BBS: GUI refactor: add obj manipulation GLGizmoSVG::GLGizmoSVG(GLCanvas3D& parent, unsigned int sprite_id) : GLGizmoBase(parent, "tool_bar_svg.svg", sprite_id) //"toolbar_cut.svg" no use , m_gui_cfg(nullptr) , m_rotate_gizmo(parent, GLGizmoRotate::Axis::Z) // grab id = 2 (Z axis) { m_rotate_gizmo.set_group_id(0); m_rotate_gizmo.set_force_local_coordinate(true); } std::string GLGizmoSVG::get_tooltip() const { return ""; } void GLGizmoSVG::data_changed(bool is_serializing) { set_volume_by_selection(); if (!is_serializing && m_volume == nullptr) close(); } void GLGizmoSVG::on_enable_grabber(unsigned int id) { m_rotate_gizmo.enable_grabber(0); } void GLGizmoSVG::on_disable_grabber(unsigned int id) { m_rotate_gizmo.disable_grabber(0); } Emboss::DataBasePtr GLGizmoSVG::create_emboss_data_base(std::shared_ptr> &cancel, ModelVolumeType volume_type, std::string_view filepath) { EmbossShape shape = select_shape(filepath); if (shape.shapes_with_ids.empty()) // canceled selection of SVG file return nullptr; // Cancel previous Job, when it is in process // worker.cancel(); --> Use less in this case I want cancel only previous EmbossJob no other jobs // Cancel only EmbossUpdateJob no others if (cancel != nullptr) cancel->store(true); // create new shared ptr to cancel new job cancel = std::make_shared>(false); std::string name = volume_name(shape); auto result = std::make_unique(name, cancel, std::move(shape)); result->is_outside = volume_type == ModelVolumeType::MODEL_PART; return result; } Emboss::CreateVolumeParams GLGizmoSVG::create_input(GLCanvas3D &canvas, ModelVolumeType volume_type) { auto gizmo = static_cast(GLGizmosManager::Svg); const GLVolume *gl_volume = canvas.get_selection().get_first_volume();//modify by bbs //get_first_hovered_gl_volume(canvas); Plater * plater = wxGetApp().plater(); auto register_mesh_pick = [this]() { register_single_mesh_pick(); }; #ifndef EXECUTE_UPDATE_ON_MAIN_THREAD return Emboss::CreateVolumeParams{canvas, plater->get_camera(), plater->build_volume(), plater->get_ui_job_worker(), register_mesh_pick, m_raycast_manager, m_raycast_condition,volume_type,gizmo,gl_volume}; #else return Emboss::CreateVolumeParams{canvas, plater->get_camera(), plater->build_volume(), volume_type, gizmo, gl_volume}; #endif } enum class IconType : unsigned { reset_value, refresh, change_file, bake, save, exclamation, lock, unlock, reflection_x, reflection_y, // automatic calc of icon's count _count }; // This configs holds GUI layout size given by translated texts. // etc. When language changes, GUI is recreated and this class constructed again, // so the change takes effect. (info by GLGizmoFdmSupports.hpp) struct GuiCfg { // Detect invalid config values when change monitor DPI double screen_scale = -1.; bool dark_mode = false; // Define bigger size(width or height) unsigned texture_max_size_px = 256; float input_width = 0.f; float input_offset = 0.f; float icon_width = 0.f; float max_tooltip_width = 0.f; // offset for checbox for lock up vector float lock_offset = 0.f; // Only translations needed for calc GUI size struct Translations { std::string depth; std::string size; std::string use_surface; std::string rotation; std::string distance; // from surface std::string mirror; }; Translations translations; }; GuiCfg create_gui_configuration() { GuiCfg cfg; // initialize by default values; float line_height = ImGui::GetTextLineHeight(); float line_height_with_spacing = ImGui::GetTextLineHeightWithSpacing(); float space = line_height_with_spacing - line_height; cfg.icon_width = std::max(std::round(line_height / 8) * 8, 8.f); GuiCfg::Translations &tr = cfg.translations; float lock_width = cfg.icon_width + 3 * space; // TRN - Input label. Be short as possible tr.depth = _u8L("Depth"); // TRN - Input label. Be short as possible tr.size = _u8L("Size"); // TRN - Input label. Be short as possible tr.use_surface = _u8L("Use surface"); // TRN - Input label. Be short as possible tr.distance = _u8L("From surface"); // TRN - Input label. Be short as possible tr.rotation = _u8L("Rotation"); // TRN - Input label. Be short as possible tr.mirror = _u8L("Mirror"); float max_tr_width = std::max({ ImGui::CalcTextSize(tr.depth.c_str()).x, ImGui::CalcTextSize(tr.size.c_str()).x + lock_width, ImGui::CalcTextSize(tr.use_surface.c_str()).x + space *2, ImGui::CalcTextSize(tr.distance.c_str()).x + space, ImGui::CalcTextSize(tr.rotation.c_str()).x + lock_width, ImGui::CalcTextSize(tr.mirror.c_str()).x, }) + space; const ImGuiStyle &style = ImGui::GetStyle(); cfg.input_offset = style.WindowPadding.x + max_tr_width + space * 2+ cfg.icon_width; cfg.lock_offset = cfg.input_offset - (cfg.icon_width + 2 * space); ImVec2 letter_m_size = ImGui::CalcTextSize("M"); const float count_letter_M_in_input = 12.f; cfg.input_width = letter_m_size.x * count_letter_M_in_input; cfg.texture_max_size_px = std::round((cfg.input_width + cfg.input_offset + cfg.icon_width + space) / 8) * 8; cfg.max_tooltip_width = ImGui::GetFontSize() * 20.0f; return cfg; } // use private definition struct GLGizmoSVG::GuiCfg : public ::GuiCfg {}; // Define rendered version of icon enum class IconState : unsigned { activable = 0, hovered /*1*/, disabled /*2*/ }; // selector for icon by enum const IconManager::Icon &get_icon(const IconManager::VIcons &icons, IconType type, IconState state) { return *icons[(unsigned) type][(unsigned) state]; } IconManager::VIcons init_icons(IconManager &mng, const GuiCfg &cfg) { mng.release(); ImVec2 size(cfg.icon_width, cfg.icon_width); // icon order has to match the enum IconType std::vector filenames{ "text_undo.svg", // reset_value "text_refresh.svg", // refresh "text_open.svg", // changhe_file "text_bake.svg", // bake "text_save.svg", // save "text_obj_warning.svg", // exclamation // ORCA: use obj_warning instead exclamation. exclamation is not compatible with low res "text_lock_closed.svg", // lock "text_lock_open.svg", // unlock "text_reflection_x.svg", // reflection_x "text_reflection_y.svg", // reflection_y }; assert(filenames.size() == static_cast(IconType::_count)); std::string path = resources_dir() + "/images/"; for (std::string &filename : filenames) filename = path + filename; auto type = IconManager::RasterType::color_wite_gray; return mng.init(filenames, size, type); } bool draw_clickable(const IconManager::VIcons &icons, IconType type) { return clickable(get_icon(icons, type, IconState::activable), get_icon(icons, type, IconState::hovered)); } bool reset_button(const IconManager::VIcons &icons) { float reset_offset = ImGui::GetStyle().WindowPadding.x; ImGui::SameLine(reset_offset); // from GLGizmoCut // std::string label_id = "neco"; // std::string btn_label; // btn_label += ImGui::RevertButton; // return ImGui::Button((btn_label + "##" + label_id).c_str()); return draw_clickable(icons, IconType::reset_value); } bool GLGizmoSVG::create_volume(ModelVolumeType volume_type) { Emboss::CreateVolumeParams input = create_input(m_parent, volume_type); Emboss::DataBasePtr base = create_emboss_data_base(m_job_cancel, volume_type); if (!base) return false; // Uninterpretable svg return start_create_volume_without_position(input, std::move(base)); } bool GLGizmoSVG::create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos) { Emboss::CreateVolumeParams input = create_input(m_parent, volume_type); Emboss::DataBasePtr base = create_emboss_data_base(m_job_cancel, volume_type); if (!base) return false; // Uninterpretable svg return start_create_volume(input, std::move(base), mouse_pos); } bool GLGizmoSVG::create_volume(std::string_view svg_file, const Vec2d &mouse_pos, ModelVolumeType volume_type) {//drag file//drag svg_file wxBusyCursor wait; Emboss::CreateVolumeParams input = create_input(m_parent, volume_type); Emboss::DataBasePtr base = create_emboss_data_base(m_job_cancel, volume_type, svg_file); if (!base) return false; // Uninterpretable svg return start_create_volume(input, std::move(base), mouse_pos); } bool GLGizmoSVG::create_volume(std::string_view svg_file, ModelVolumeType volume_type) { Emboss::CreateVolumeParams input = create_input(m_parent, volume_type); Emboss::DataBasePtr base = create_emboss_data_base(m_job_cancel, volume_type, svg_file); if (!base) return false; // Uninterpretable svg return start_create_volume_without_position(input, std::move(base)); } bool GLGizmoSVG::is_svg_object(const ModelVolume &volume) { if (!volume.emboss_shape.has_value()) return false; if (volume.type() != ModelVolumeType::MODEL_PART) return false; for (const ModelVolume *v : volume.get_object()->volumes) { if (v->id() == volume.id()) continue; if (v->type() == ModelVolumeType::MODEL_PART) return false; } return true; } bool GLGizmoSVG::is_svg(const ModelVolume &volume) { return volume.emboss_shape.has_value() && volume.emboss_shape->svg_file.has_value(); } bool GLGizmoSVG::on_init() { m_rotate_gizmo.init(); ColorRGBA gray_color(.6f, .6f, .6f, .3f); m_rotate_gizmo.set_highlight_color(gray_color.get_data()); // Set rotation gizmo upwardrotate m_rotate_gizmo.set_angle(PI / 2); return true; } std::string GLGizmoSVG::on_get_name() const { return "SVG"; } bool GLGizmoSVG::on_is_activable() const { const Selection &selection = m_parent.get_selection(); if (selection.is_empty()) { return true; } return !selection.is_any_connector();//maybe is negitive volume or modifier volume } void GLGizmoSVG::on_set_state() { // enable / disable bed from picking // Rotation gizmo must work through bed // m_parent.set_raycaster_gizmos_on_top(GLGizmoBase::m_state == GLGizmoBase::On); m_rotate_gizmo.set_state(GLGizmoBase::m_state); // Closing gizmo. e.g. selecting another one if (GLGizmoBase::m_state == GLGizmoBase::Off) { ImGui::FocusWindow(nullptr); // exit cursor m_parent.enable_moving(true); // modify by bbs reset_volume(); } else if (GLGizmoBase::m_state == GLGizmoBase::On) { if (on_is_activable()) { // Try(when exist) set text configuration by volume set_volume_by_selection(); if (m_volume) { register_single_mesh_pick(); } } } } void GLGizmoSVG::on_start_dragging() { if (m_hover_id < 0) { return;} if (m_hover_id == c_move_cube_id) { } else { m_rotate_gizmo.start_dragging(); } } void GLGizmoSVG::on_stop_dragging() { if (m_hover_id == c_move_cube_id) { } else { m_rotate_gizmo.stop_dragging(); // TODO: when start second rotatiton previous rotation rotate draggers // This is fast fix for second try to rotate // When fixing, move grabber above text (not on side) m_rotate_gizmo.set_angle(PI / 2); // apply rotation // TRN This is an item label in the undo-redo stack. m_parent.do_rotate(rotation_snapshot_name); m_rotate_start_angle.reset(); volume_transformation_changed(); // recalculate for surface cut if (m_volume != nullptr && m_volume->emboss_shape.has_value() && m_volume->emboss_shape->projection.use_surface) process_job(); } } bool GLGizmoSVG::on_mouse(const wxMouseEvent &mouse_event) { // not selected volume if (m_volume == nullptr || get_model_volume(m_volume_id, m_parent.get_selection().get_model()->objects) == nullptr || !m_volume->emboss_shape.has_value()) return false; if (m_hover_id != c_move_cube_id) { if (on_mouse_for_rotation(mouse_event)) return true; } if (m_hover_id == c_move_cube_id) { if (mouse_event.Moving()) return false; bool used = use_grabbers(mouse_event); if (on_mouse_for_translate(mouse_event)) return true; } return false; } void GLGizmoSVG::on_update(const UpdateData &data) { if (m_hover_id == c_move_cube_id) { } else { m_rotate_gizmo.update(data); } } void GLGizmoSVG::on_render() { if (const Selection &selection = m_parent.get_selection(); selection.volumes_count() != 1 || // only one selected volume m_volume == nullptr || // already selected volume in gizmo get_model_volume(m_volume_id, selection.get_model()->objects) == nullptr) // still exist model return; if (!m_svg_volume) { return; } bool is_surface_dragging = m_surface_drag.has_value(); bool is_parent_dragging = m_parent.is_mouse_dragging(); // Do NOT render rotation grabbers when dragging object bool is_rotate_by_grabbers = m_dragging; glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); Geometry::Transformation tran(m_svg_volume->world_matrix()); if (!m_volume->is_the_only_one_part()) { m_parent.enable_moving(false); // modify by bbs bool hover = (m_hover_id == c_move_cube_id); std::array render_color; if (hover) { render_color = SVG_Move_GrabberHoverColor; } else render_color = SVG_Move_GrabberColor; float fullsize = get_grabber_size(); m_move_grabber.center = tran.get_offset(); Transform3d rotate_matrix = tran.get_rotation_matrix(); Transform3d cube_mat = Geometry::translation_transform(m_move_grabber.center) * rotate_matrix * Geometry::scale_transform(fullsize); m_move_grabber.set_model_matrix(cube_mat); 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_move_grabber.get_cube(), render_color, view_matrix * cube_mat, projection_matrix); } #ifdef DEBUG_SVG render_cross_mark(tran.get_matrix(), Vec3f::Zero(), true); #endif if (is_rotate_by_grabbers || (!is_surface_dragging && !is_parent_dragging)) { if (m_hover_id == c_move_cube_id && m_dragging) { } else { m_rotate_gizmo.render(); } } } void GLGizmoSVG::on_render_for_picking() { glsafe(::glDisable(GL_DEPTH_TEST)); m_rotate_gizmo.render_for_picking(); auto color = picking_color_component(c_move_cube_id); m_move_grabber.color[0] = color[0]; m_move_grabber.color[1] = color[1]; m_move_grabber.color[2] = color[2]; m_move_grabber.color[3] = color[3]; const auto& shader = wxGetApp().get_shader("flat"); if (nullptr != shader) { wxGetApp().bind_shader(shader); m_move_grabber.render_for_picking(); wxGetApp().unbind_shader(); } } //BBS: add input window for move void GLGizmoSVG::on_render_input_window(float x, float y, float bottom_limit) { set_volume_by_selection(); double screen_scale = wxDisplay(wxGetApp().plater()).GetScaleFactor(); // Configuration creation if (m_gui_cfg == nullptr || // Exist configuration - first run m_gui_cfg->screen_scale != screen_scale || // change of DPI m_gui_cfg->dark_mode != m_is_dark_mode // change of dark mode ) { // Create cache for gui offsets ::GuiCfg cfg = create_gui_configuration(); cfg.screen_scale = screen_scale; cfg.dark_mode = m_is_dark_mode; GuiCfg gui_cfg{std::move(cfg)}; m_gui_cfg = std::make_unique(std::move(gui_cfg)); m_icons = init_icons(m_icon_manager, *m_gui_cfg); // need regeneration when change resolution(move between monitors) } static float last_y = 0.0f; static float last_h = 0.0f; const float win_h = ImGui::GetWindowHeight(); y = std::min(y, bottom_limit - win_h); GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 0.0f, 0.0f); if (last_h != win_h || last_y != y) { // ask canvas for another frame to render the window in the correct position m_imgui->set_requires_extra_frame(); if (last_h != win_h) last_h = win_h; if (last_y != y) last_y = y; } ImGuiWrapper::push_toolbar_style(m_parent.get_scale()); GizmoImguiBegin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); draw_window(); GizmoImguiEnd(); ImGuiWrapper::pop_toolbar_style(); } void GLGizmoSVG::set_volume_by_selection() { const Selection &selection = m_parent.get_selection(); const GLVolume * gl_volume = get_selected_gl_volume(selection); if (gl_volume == nullptr) return reset_volume(); const ModelObjectPtrs &objects = selection.get_model()->objects; ModelVolume * volume = get_model_volume(*gl_volume, objects); if (volume == nullptr) return reset_volume(); // is same volume as actual selected? if (volume->id() == m_volume_id) return; // Do not use focused input value when switch volume(it must swith value) if (m_volume != nullptr && m_volume != volume) // when update volume it changed id BUT not pointer ImGuiWrapper::left_inputs(); // is valid svg volume? if (!is_svg(*volume)) return reset_volume(); // cancel previous job if (m_job_cancel != nullptr) { m_job_cancel->store(true); m_job_cancel = nullptr; } // calculate scale for height and depth inside of scaled object instance calculate_scale(); // must be before calculation of tesselation // checking that exist is inside of function "is_svg" EmbossShape & es = *volume->emboss_shape; EmbossShape::SvgFile &svg_file = *es.svg_file; if (svg_file.image == nullptr) { if (init_image(svg_file) == nullptr) return reset_volume(); } assert(svg_file.image != nullptr); assert(svg_file.image.get() != nullptr); const NSVGimage & image = *svg_file.image; ExPolygonsWithIds &shape_ids = es.shapes_with_ids; if (shape_ids.empty()) { NSVGLineParams params{get_tesselation_tolerance(get_scale_for_tolerance())}; shape_ids = create_shape_with_ids(image, params); } reset_volume(); // clear cached data m_svg_volume = gl_volume; m_volume = volume; m_volume_id = volume->id(); m_volume_shape = es; // copy m_shape_warnings = create_shape_warnings(es, get_scale_for_tolerance()); // Calculate current angle of up vector m_angle = calc_angle(selection); m_distance = calc_distance(*gl_volume, m_raycast_manager, m_parent); m_shape_bb = get_extents(m_volume_shape.shapes_with_ids); m_origin_shape_bb = m_shape_bb; } void GLGizmoSVG::delete_texture(Emboss::Texture &texture) { if (texture.id != 0) { glsafe(::glDeleteTextures(1, &texture.id)); texture.id = 0; } } void GLGizmoSVG::reset_volume() { if (m_volume == nullptr) return; // already reseted m_svg_volume = nullptr; m_volume = nullptr; m_volume_id.id = 0; m_volume_shape.shapes_with_ids.clear(); m_filename_preview.clear(); m_shape_warnings.clear(); // delete texture after finish imgui draw wxGetApp().plater()->CallAfter([this]() { delete_texture(m_texture); }); } void GLGizmoSVG::calculate_scale() { // be carefull m_volume is not set yet const Selection &selection = m_parent.get_selection(); const GLVolume * gl_volume = selection.get_first_volume(); if (gl_volume == nullptr) return; Transform3d to_world = gl_volume->world_matrix(); const ModelVolume *volume_ptr = get_model_volume(*gl_volume, selection.get_model()->objects); assert(volume_ptr != nullptr); assert(volume_ptr->emboss_shape.has_value()); // Fix for volume loaded from 3mf if (volume_ptr != nullptr && volume_ptr->emboss_shape.has_value()) { const std::optional &fix_tr = volume_ptr->emboss_shape->fix_3mf_tr; if (fix_tr.has_value()) to_world = to_world * (fix_tr->inverse()); } auto to_world_linear = to_world.linear(); auto calc = [&to_world_linear](const Vec3d &axe, std::optional &scale) { Vec3d axe_world = to_world_linear * axe; double norm_sq = axe_world.squaredNorm(); if (is_approx(norm_sq, 1.)) { if (!scale.has_value()) return; scale.reset(); } else { scale = sqrt(norm_sq); } }; calc(Vec3d::UnitX(), m_scale_width); calc(Vec3d::UnitY(), m_scale_height); calc(Vec3d::UnitZ(), m_scale_depth); } // inspired by Xiaolin Wu's line algorithm - https://en.wikipedia.org/wiki/Xiaolin_Wu's_line_algorithm // Draw inner part of polygon CCW line as full brightness(edge of expolygon) void wu_draw_line_side(Linef line, const std::function &plot) { auto ipart = [](float x) -> int { return static_cast(std::floor(x)); }; auto round = [](float x) -> float { return std::round(x); }; auto fpart = [](float x) -> float { return x - std::floor(x); }; auto rfpart = [=](float x) -> float { return 1 - fpart(x); }; Vec2d d = line.b - line.a; const bool steep = abs(d.y()) > abs(d.x()); bool is_full; // identify full brightness pixel if (steep) { is_full = d.y() >= 0; std::swap(line.a.x(), line.a.y()); std::swap(line.b.x(), line.b.y()); std::swap(d.x(), d.y()); } else is_full = d.x() < 0; // opposit direction of y if (line.a.x() > line.b.x()) { std::swap(line.a.x(), line.b.x()); std::swap(line.a.y(), line.b.y()); d *= -1; } const float gradient = (d.x() == 0) ? 1. : d.y() / d.x(); int xpx11; float intery; { const float xend = round(line.a.x()); const float yend = line.a.y() + gradient * (xend - line.a.x()); const float xgap = rfpart(line.a.x() + 0.5f); xpx11 = int(xend); const int ypx11 = ipart(yend); if (steep) { plot(ypx11, xpx11, is_full ? 1.f : (rfpart(yend) * xgap)); plot(ypx11 + 1, xpx11, !is_full ? 1.f : (fpart(yend) * xgap)); } else { plot(xpx11, ypx11, is_full ? 1.f : (rfpart(yend) * xgap)); plot(xpx11, ypx11 + 1, !is_full ? 1.f : (fpart(yend) * xgap)); } intery = yend + gradient; } int xpx12; { const float xend = round(line.b.x()); const float yend = line.b.y() + gradient * (xend - line.b.x()); const float xgap = rfpart(line.b.x() + 0.5f); xpx12 = int(xend); const int ypx12 = ipart(yend); if (steep) { plot(ypx12, xpx12, is_full ? 1.f : (rfpart(yend) * xgap)); plot(ypx12 + 1, xpx12, !is_full ? 1.f : (fpart(yend) * xgap)); } else { plot(xpx12, ypx12, is_full ? 1.f : (rfpart(yend) * xgap)); plot(xpx12, ypx12 + 1, !is_full ? 1.f : (fpart(yend) * xgap)); } } if (steep) { if (is_full) { for (int x = xpx11 + 1; x < xpx12; x++) { plot(ipart(intery), x, 1.f); plot(ipart(intery) + 1, x, fpart(intery)); intery += gradient; } } else { for (int x = xpx11 + 1; x < xpx12; x++) { plot(ipart(intery), x, rfpart(intery)); plot(ipart(intery) + 1, x, 1.f); intery += gradient; } } } else { if (is_full) { for (int x = xpx11 + 1; x < xpx12; x++) { plot(x, ipart(intery), 1.f); plot(x, ipart(intery) + 1, fpart(intery)); intery += gradient; } } else { for (int x = xpx11 + 1; x < xpx12; x++) { plot(x, ipart(intery), rfpart(intery)); plot(x, ipart(intery) + 1, 1.f); intery += gradient; } } } } template // N .. count of channels per pixel void draw_side_outline(const ExPolygons &shape, const std::array &color, std::vector &data, size_t data_width, double scale) { int count_lines = data.size() / (N * data_width); size_t data_line = N * data_width; auto get_offset = [count_lines, data_line](int x, int y) { // NOTE: y has opposit direction in texture return (count_lines - y - 1) * data_line + x * N; }; // overlap color auto draw = [&data, data_width, count_lines, get_offset, &color](int x, int y, float brightess) { if (x < 0 || y < 0 || static_cast(x) >= data_width || y >= count_lines) return; // out of image size_t offset = get_offset(x, y); bool change_color = false; for (size_t i = 0; i < N - 1; ++i) { if (data[offset + i] != color[i]) { data[offset + i] = color[i]; change_color = true; } } unsigned char &alpha = data[offset + N - 1]; if (alpha == 0 || change_color) { alpha = static_cast(std::round(brightess * 255)); } else if (alpha != 255) { alpha = static_cast(std::min(255, int(alpha) + static_cast(std::round(brightess * 255)))); } }; BoundingBox bb_unscaled = get_extents(shape); Linesf lines = to_linesf(shape); BoundingBoxf bb(bb_unscaled.min.cast(), bb_unscaled.max.cast()); // scale lines to pixels if (!is_approx(scale, 1.)) { for (Linef &line : lines) { line.a *= scale; line.b *= scale; } bb.min *= scale; bb.max *= scale; } for (const Linef &line : lines) wu_draw_line_side(line, draw); } /// /// Draw filled ExPolygon into data /// line by line inspired by: http://alienryderflex.com/polygon_fill/ /// /// Count channels for one pixel(RGBA = 4) /// Shape to draw /// Color of shape contain count of channels(N) /// Image(2d) stored in 1d array /// Count of pixel on one line(size in data = N x data_width) /// Shape scale for conversion to pixels template // N .. count of channels per pixel void draw_filled(const ExPolygons &shape, const std::array &color, std::vector &data, size_t data_width, double scale) { assert(data.size() % N == 0); assert(data.size() % data_width == 0); assert((data.size() % (N * data_width)) == 0); BoundingBox bb_unscaled = get_extents(shape); Linesf lines = to_linesf(shape); BoundingBoxf bb(bb_unscaled.min.cast(), bb_unscaled.max.cast()); // scale lines to pixels if (!is_approx(scale, 1.)) { for (Linef &line : lines) { line.a *= scale; line.b *= scale; } bb.min *= scale; bb.max *= scale; } int count_lines = data.size() / (N * data_width); size_t data_line = N * data_width; auto get_offset = [count_lines, data_line](int x, int y) { // NOTE: y has opposit direction in texture return (count_lines - y - 1) * data_line + x * N; }; auto set_color = [&data, &color, get_offset](int x, int y) { size_t offset = get_offset(x, y); if (data[offset + N - 1] != 0) return; // already setted by line for (unsigned i = 0; i < N; ++i) data[offset + i] = color[i]; }; // anti aliased drawing of lines auto draw = [&data, width = static_cast(data_width), count_lines, get_offset, &color](int x, int y, float brightess) { if (x < 0 || y < 0 || x >= width || y >= count_lines) return; // out of image size_t offset = get_offset(x, y); unsigned char &alpha = data[offset + N - 1]; if (alpha == 0) { alpha = static_cast(std::round(brightess * 255)); for (size_t i = 0; i < N - 1; ++i) data[offset + i] = color[i]; } else if (alpha != 255) { alpha = static_cast(std::min(255, int(alpha) + static_cast(std::round(brightess * 255)))); } }; for (const Linef &line : lines) wu_draw_line_side(line, draw); auto tree = Slic3r::AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); // range for intersection line double x1 = bb.min.x() - 1.f; double x2 = bb.max.x() + 1.f; int max_y = std::min(count_lines, static_cast(std::round(bb.max.y()))); for (int y = std::max(0, static_cast(std::round(bb.min.y()))); y < max_y; ++y) { double y_f = y + .5; // 0.5 ... intersection in center of pixel of pixel Linef line(Vec2d(x1, y_f), Vec2d(x2, y_f)); using Intersection = std::pair; using Intersections = std::vector; // sorted .. false // Intersections intersections = Slic3r::AABBTreeLines::get_intersections_with_line(lines, tree, line); if (intersections.empty()) continue; assert((intersections.size() % 2) == 0); // sort intersections by x std::sort(intersections.begin(), intersections.end(), [](const Intersection &i1, const Intersection &i2) { return i1.first.x() < i2.first.x(); }); // draw lines for (size_t i = 0; i < intersections.size(); i += 2) { const Vec2d &p2 = intersections[i + 1].first; if (p2.x() < 0) continue; // out of data const Vec2d &p1 = intersections[i].first; if (p1.x() > data_width) break; // out of data // clamp to data int max_x = std::min(static_cast(data_width - 1), static_cast(std::round(p2.x()))); for (int x = std::max(0, static_cast(std::round(p1.x()))); x <= max_x; ++x) set_color(x, y); } } } /// Union shape defined by glyphs ExPolygons union_ex(const ExPolygonsWithIds &shapes) { // unify to one expolygon ExPolygons result; for (const ExPolygonsWithId &shape : shapes) { if (shape.expoly.empty()) continue; expolygons_append(result, shape.expoly); } return union_ex(result); } // init texture by draw expolygons into texture bool GLGizmoSVG::init_texture(Emboss::Texture &texture, const ExPolygonsWithIds &shapes_with_ids, unsigned max_size_px, const std::vector &shape_warnings) { BoundingBox bb = get_extents(shapes_with_ids); Point bb_size = bb.size(); double bb_width = bb_size.x(); // [in mm] double bb_height = bb_size.y(); // [in mm] bool is_widder = bb_size.x() > bb_size.y(); double scale = 0.f; if (is_widder) { scale = max_size_px / bb_width; texture.width = max_size_px; texture.height = static_cast(std::ceil(bb_height * scale)); } else { scale = max_size_px / bb_height; texture.width = static_cast(std::ceil(bb_width * scale)); texture.height = max_size_px; } const int n_pixels = texture.width * texture.height; if (n_pixels <= 0) return false; constexpr int channels_count = 4; std::vector data(n_pixels * channels_count, {0}); // Union All shapes ExPolygons shape = union_ex(shapes_with_ids); // align to texture translate(shape, -bb.min); size_t texture_width = static_cast(texture.width); unsigned char alpha = 255; // without transparency std::array color_shape{201, 201, 201, alpha}; // from degin by @JosefZachar std::array color_error{237, 28, 36, alpha}; // from icon: resources/icons/flag_red.svg std::array color_warning{237, 107, 33, alpha}; // icons orange // draw unhealedable shape for (const ExPolygonsWithId &shapes_with_id : shapes_with_ids) if (!shapes_with_id.is_healed) { ExPolygons bad_shape = shapes_with_id.expoly; // copy translate(bad_shape, -bb.min); // align to texture draw_side_outline<4>(bad_shape, color_error, data, texture_width, scale); } // Draw shape with warning if (!shape_warnings.empty()) { for (const ExPolygonsWithId &shapes_with_id : shapes_with_ids) { assert(shapes_with_id.id < shape_warnings.size()); if (shapes_with_id.id >= shape_warnings.size()) continue; if (shape_warnings[shapes_with_id.id].empty()) continue; // no warnings for shape ExPolygons warn_shape = shapes_with_id.expoly; // copy translate(warn_shape, -bb.min); // align to texture draw_side_outline<4>(warn_shape, color_warning, data, texture_width, scale); } } // Draw rest of shape draw_filled<4>(shape, color_shape, data, texture_width, scale); // sends data to gpu glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); if (texture.id != 0) glsafe(::glDeleteTextures(1, &texture.id)); glsafe(::glGenTextures(1, &texture.id)); glsafe(::glBindTexture(GL_TEXTURE_2D, texture.id)); glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei) texture.width, (GLsizei) texture.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void *) data.data())); glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); GLuint NO_TEXTURE_ID = 0; glsafe(::glBindTexture(GL_TEXTURE_2D, NO_TEXTURE_ID)); return true; } bool is_closed(NSVGpath *path) { for (; path != NULL; path = path->next) if (path->next == NULL && path->closed) return true; return false; } void add_comma_separated(std::string &result, const std::string &add) { if (!result.empty()) result += ", "; result += add; } const float warning_preccission = 1e-4f; std::string create_fill_warning(const NSVGshape &shape) { if (!(shape.flags & NSVG_FLAGS_VISIBLE) || shape.fill.type == NSVG_PAINT_NONE) return {}; // not visible std::string warning; if ((shape.opacity - 1.f + warning_preccission) <= 0.f) add_comma_separated(warning, GUI::format(_L("Opacity (%1%)"), shape.opacity)); // if(shape->flags != NSVG_FLAGS_VISIBLE) add_warning(_u8L("Visibility flag")); bool is_fill_gradient = shape.fillGradient[0] != '\0'; if (is_fill_gradient) add_comma_separated(warning, GUI::format(_L("Color gradient (%1%)"), shape.fillGradient)); switch (shape.fill.type) { case NSVG_PAINT_UNDEF: add_comma_separated(warning, _u8L("Undefined fill type")); break; case NSVG_PAINT_LINEAR_GRADIENT: if (!is_fill_gradient) add_comma_separated(warning, _u8L("Linear gradient")); break; case NSVG_PAINT_RADIAL_GRADIENT: if (!is_fill_gradient) add_comma_separated(warning, _u8L("Radial gradient")); break; // case NSVG_PAINT_NONE: // case NSVG_PAINT_COLOR: // default: break; } // Unfilled is only line which could be opened if (shape.fill.type != NSVG_PAINT_NONE && !is_closed(shape.paths)) add_comma_separated(warning, _u8L("Open filled path")); return warning; } std::string create_stroke_warning(const NSVGshape &shape) { std::string warning; if (!(shape.flags & NSVG_FLAGS_VISIBLE) || shape.stroke.type == NSVG_PAINT_NONE || shape.strokeWidth <= 1e-5f) return {}; // not visible if ((shape.opacity - 1.f + warning_preccission) <= 0.f) add_comma_separated(warning, GUI::format(_L("Opacity (%1%)"), shape.opacity)); bool is_stroke_gradient = shape.strokeGradient[0] != '\0'; if (is_stroke_gradient) add_comma_separated(warning, GUI::format(_L("Color gradient (%1%)"), shape.strokeGradient)); switch (shape.stroke.type) { case NSVG_PAINT_UNDEF: add_comma_separated(warning, _u8L("Undefined stroke type")); break; case NSVG_PAINT_LINEAR_GRADIENT: if (!is_stroke_gradient) add_comma_separated(warning, _u8L("Linear gradient")); break; case NSVG_PAINT_RADIAL_GRADIENT: if (!is_stroke_gradient) add_comma_separated(warning, _u8L("Radial gradient")); break; // case NSVG_PAINT_COLOR: // case NSVG_PAINT_NONE: // default: break; } return warning; } float GLGizmoSVG::get_scale_for_tolerance() { return std::max(m_scale_width.value_or(1.f), m_scale_height.value_or(1.f)); } /// /// Create warnings about shape /// /// Input svg loaded to shapes /// Vector of warnings with same size as EmbossShape::shapes_with_ids /// or Empty when no warnings -> for fast checking that every thing is all right(more common case) std::vector GLGizmoSVG::create_shape_warnings(const EmbossShape &shape, float scale) { const std::shared_ptr &image_ptr = shape.svg_file->image; assert(image_ptr != nullptr); if (image_ptr == nullptr) return {std::string{"Uninitialized SVG image"}}; const NSVGimage & image = *image_ptr; std::vector result; auto add_warning = [&result, &image](size_t index, const std::string &message) { if (result.empty()) result = std::vector(get_shapes_count(image) * 2); std::string &res = result[index]; if (res.empty()) res = message; else res += '\n' + message; }; if (!shape.final_shape.is_healed) { for (const ExPolygonsWithId &i : shape.shapes_with_ids) if (!i.is_healed) add_warning(i.id, _u8L("Path can't be healed from selfintersection and multiple points.")); // This waning is not connected to NSVGshape. It is about union of paths, but Zero index is shown first size_t index = 0; add_warning(index, _u8L("Final shape constains selfintersection or multiple points with same coordinate.")); } size_t shape_index = 0; for (NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next, ++shape_index) { if (!(shape->flags & NSVG_FLAGS_VISIBLE)) { add_warning(shape_index * 2, GUI::format(_L("Shape is marked as invisible (%1%)."), shape->id)); continue; } std::string fill_warning = create_fill_warning(*shape); if (!fill_warning.empty()) { // TRN: The first placeholder is shape identifier, the second one is text describing the problem. add_warning(shape_index * 2, GUI::format(_L("Fill of shape (%1%) contains unsupported: %2%."), shape->id, fill_warning)); } float minimal_width_in_mm = 1e-3f; if (shape->strokeWidth <= minimal_width_in_mm * scale) { add_warning(shape_index * 2, GUI::format(_L("Stroke of shape (%1%) is too thin (minimal width is %2% mm)."), shape->id, minimal_width_in_mm)); continue; } std::string stroke_warning = create_stroke_warning(*shape); if (!stroke_warning.empty()) add_warning(shape_index * 2 + 1, GUI::format(_L("Stroke of shape (%1%) contains unsupported: %2%."), shape->id, stroke_warning)); } return result; } bool GLGizmoSVG::process_job(bool make_snapshot) { // no volume is selected -> selection from right panel assert(m_volume != nullptr); if (m_volume == nullptr) return false; assert(m_volume->emboss_shape.has_value()); if (!m_volume->emboss_shape.has_value()) return false; // Cancel previous Job, when it is in process // worker.cancel(); --> Use less in this case I want cancel only previous EmbossJob no other jobs // Cancel only EmbossUpdateJob no others if (m_job_cancel != nullptr) m_job_cancel->store(true); // create new shared ptr to cancel new job m_job_cancel = std::make_shared>(false); EmbossShape shape = m_volume_shape; // copy auto base = std::make_unique(m_volume->name, m_job_cancel, std::move(shape)); base->is_outside = m_volume->type() == ModelVolumeType::MODEL_PART; Emboss::DataUpdate data{std::move(base), m_volume_id, make_snapshot}; return start_update_volume(std::move(data), *m_volume, m_parent.get_selection(), m_raycast_manager); } void GLGizmoSVG::close() { // close gizmo == open it again auto &mng = m_parent.get_gizmos_manager(); if (mng.get_current_type() == GLGizmosManager::Svg) mng.open_gizmo(GLGizmosManager::Svg); reset_volume(); } void GLGizmoSVG::draw_window() { assert(m_volume != nullptr); assert(m_volume_id.valid()); if (m_volume == nullptr || m_volume_id.invalid()) { m_imgui->text(_L("Not valid state please report reproduction steps on github")); return; } assert(m_volume->emboss_shape.has_value()); if (!m_volume->emboss_shape.has_value()) { m_imgui->text(_L("No embossed file")); return; } assert(m_volume->emboss_shape->svg_file.has_value()); if (!m_volume->emboss_shape->svg_file.has_value()) { m_imgui->text(_L("Missing svg file in embossed shape")); return; } assert(m_volume->emboss_shape->svg_file->file_data != nullptr); if (m_volume->emboss_shape->svg_file->file_data == nullptr) { m_imgui->text(_L("Missing data of svg file")); return; } draw_preview(); draw_filename(); // Is SVG baked? if (m_volume == nullptr) return; ImGui::Separator(); draw_depth(); draw_size(); m_can_use_surface = (m_volume->emboss_shape->projection.use_surface) ? true : // already used surface must have option to uncheck !m_volume->is_the_only_one_part(); if (m_can_use_surface) { draw_use_surface(); draw_distance(); } draw_rotation(); draw_mirroring(); //draw_face_the_camera(); if (!m_volume->is_the_only_one_part()) { ImGui::Separator(); draw_model_type(); } if (!m_can_use_surface) { m_imgui->text_wrapped(_L("Tip:If you want to place svg file on another part surface,you should select part first, and then drag svg file to the part surface."), m_gui_cfg->input_offset + m_gui_cfg->input_width + m_gui_cfg->icon_width); } } void GLGizmoSVG::draw_preview() {// init texture when not initialized yet. // drag&drop is out of rendering scope so texture must be created on this place if (m_texture.id == 0) { const ExPolygonsWithIds &shapes = m_volume->emboss_shape->shapes_with_ids; init_texture(m_texture, shapes, m_gui_cfg->texture_max_size_px, m_shape_warnings); } //::draw(m_volume_shape.shapes_with_ids, m_gui_cfg->texture_max_size_px); if (m_texture.id != 0) { ImTextureID id = (void *) static_cast(m_texture.id); unsigned window_width = static_cast(ImGui::GetWindowSize().x - 2 * ImGui::GetStyle().WindowPadding.x); if (m_texture.width > window_width && window_width > 0) { m_texture.width = window_width; } if (m_texture.height > m_texture.width) { m_texture.height = m_texture.width; } ImVec2 s(m_texture.width, m_texture.height); //std::optional spacing; // is texture over full height? /* if (m_texture.height != m_gui_cfg->texture_max_size_px) { spacing = (m_gui_cfg->texture_max_size_px - m_texture.height) / 2.f; ImGui::SetCursorPosY(ImGui::GetCursorPosY() + *spacing); }*/ if (window_width > m_texture.width) { float space = (window_width - m_texture.width) / 2.f; ImGui::SetCursorPosX(ImGui::GetCursorPosX() + space); } ImGui::Image(id, s); // if(ImGui::IsItemHovered()){ // const EmbossShape &es = *m_volume->emboss_shape; // size_t count_of_shapes = get_shapes_count(*es.svg_file.image); // size_t count_of_expolygons = 0; // size_t count_of_points = 0; // for (const auto &shape : es.shapes_with_ids) { // for (const ExPolygon &expoly : shape.expoly){ // ++count_of_expolygons; // count_of_points += count_points(expoly); // } // } // // Do not translate it is only for debug // std::string tooltip = GUI::format("%1% shapes, which create %2% polygons with %3% line segments", // count_of_shapes, count_of_expolygons, count_of_points); // ImGui::SetTooltip("%s", tooltip.c_str()); //} /* if (spacing.has_value()) ImGui::SetCursorPosY(ImGui::GetCursorPosY() + *spacing);*/ } } void GLGizmoSVG::draw_filename() { const EmbossShape & es = *m_volume->emboss_shape; const EmbossShape::SvgFile &svg = *es.svg_file; if (m_filename_preview.empty()) { // create filename preview if (!svg.path.empty()) { m_filename_preview = get_file_name(svg.path); } else if (!svg.path_in_3mf.empty()) { m_filename_preview = get_file_name(svg.path_in_3mf); } if (m_filename_preview.empty()) // TRN - Preview of filename after clear local filepath. m_filename_preview = _u8L("Unknown filename"); m_filename_preview = ImGuiWrapper::trunc(m_filename_preview, m_gui_cfg->input_width); } if (!m_shape_warnings.empty()) { draw(get_icon(m_icons, IconType::exclamation, IconState::hovered)); if (ImGui::IsItemHovered()) { std::string tooltip; for (const std::string &w : m_shape_warnings) { if (w.empty()) continue; if (!tooltip.empty()) tooltip += "\n"; tooltip += w; } m_imgui->tooltip(tooltip, m_gui_cfg->max_tooltip_width); } ImGui::SameLine(); } // Remove space between filename and gray suffix ".svg" ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); ImGui::AlignTextToFramePadding(); ImGui::Text("%s", m_filename_preview.c_str()); bool is_hovered = ImGui::IsItemHovered(); ImGui::SameLine(); m_imgui->text_colored(ImGuiWrapper::COL_GREY_LIGHT, ".svg"); ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing is_hovered |= ImGui::IsItemHovered(); if (is_hovered) { wxString tooltip = GUI::format_wxstr(_L("SVG file path is \"%1%\""), svg.path); m_imgui->tooltip(tooltip, m_gui_cfg->max_tooltip_width); } bool file_changed = false; // Re-Load button bool can_reload = !m_volume_shape.svg_file->path.empty(); if (can_reload) { ImGui::SameLine(); if (draw_clickable(m_icons, IconType::refresh)) { if (!boost::filesystem::exists(m_volume_shape.svg_file->path)) { m_volume_shape.svg_file->path.clear(); } else { file_changed = true; } } else if (ImGui::IsItemHovered()) m_imgui->tooltip(_L("Reload SVG file from disk."), m_gui_cfg->max_tooltip_width); } wxString tooltip = ""; ImGuiComboFlags flags = ImGuiComboFlags_PopupAlignLeft | ImGuiComboFlags_NoPreview; ImGui::SameLine(); ImGuiWrapper::push_combo_style(m_parent.get_scale()); if (ImGui::BeginCombo("##file_options", nullptr, flags)) { ScopeGuard combo_sg([]() { ImGui::EndCombo(); }); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ImGui::GetStyle().FramePadding.x, 0}); draw(get_icon(m_icons, IconType::change_file, IconState::hovered)); ImGui::SameLine(); if (ImGui::Selectable((_L("Change file") + dots).ToUTF8().data())) { std::string new_path = choose_svg_file(); if (!new_path.empty()) { file_changed = true; EmbossShape::SvgFile svg_file_new; svg_file_new.path = new_path; m_volume_shape.svg_file = svg_file_new; // clear data } } else if (ImGui::IsItemHovered()) { tooltip = _L("Change to another .svg file"); } //std::string forget_path = _u8L("Forget the file path"); //if (m_volume->emboss_shape->svg_file->path.empty()) { // draw(get_icon(m_icons, IconType::bake, IconState::disabled)); // ImGui::SameLine(); // m_imgui->text_colored(ImGuiWrapper::COL_GREY_DARK, forget_path.c_str()); //} else { // draw(get_icon(m_icons, IconType::bake, IconState::hovered)); // ImGui::SameLine(); // if (ImGui::Selectable(forget_path.c_str())) { // // set .svg_file.path_in_3mf to remember file name // m_volume->emboss_shape->svg_file->path.clear(); // m_volume_shape.svg_file->path.clear(); // m_filename_preview.clear(); // } else if (ImGui::IsItemHovered()) { // tooltip = _L("Do NOT save local path to 3MF file.\n" // "Also disables 'reload from disk' option."); // } //} draw(get_icon(m_icons, IconType::bake, IconState::hovered)); ImGui::SameLine(); // TRN: An menu option to convert the SVG into an unmodifiable model part. if (ImGui::Selectable(_u8L("Bake to model").c_str())) { m_volume->emboss_shape.reset(); close(); } else if (ImGui::IsItemHovered()) { // TRN: Tooltip for the menu item. tooltip = _L("Bake into model as uneditable part"); } draw(get_icon(m_icons, IconType::save, IconState::activable)); ImGui::SameLine(); if (ImGui::Selectable((_L("Save as") + dots).ToUTF8().data())) { wxWindow * parent = nullptr; GUI::FileType file_type = FT_SVG; wxString wildcard = file_wildcards(file_type); wxString dlg_title = _L("Save SVG file"); const EmbossShape::SvgFile &svg = *m_volume_shape.svg_file; wxString dlg_file = from_u8(get_file_name(((!svg.path.empty()) ? svg.path : svg.path_in_3mf))) + ".svg"; wxFileDialog dlg(parent, dlg_title, last_used_directory, dlg_file, wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if (dlg.ShowModal() == wxID_OK) { last_used_directory = dlg.GetDirectory(); wxString out_path = dlg.GetPath(); std::string path{out_path.c_str()}; // Slic3r::save(*m_volume_shape.svg_file.image, path); std::ofstream stream(path); if (stream.is_open()) { stream << *svg.file_data; // change source file m_filename_preview.clear(); m_volume_shape.svg_file->path = path; m_volume_shape.svg_file->path_in_3mf.clear(); // possible change name m_volume->emboss_shape->svg_file = m_volume_shape.svg_file; // copy - write changes into volume } else { BOOST_LOG_TRIVIAL(error) << "Opening file: \"" << path << "\" Failed"; } } } else if (ImGui::IsItemHovered()) { tooltip = _L("Save as '.svg' file"); } // draw(get_icon(m_icons, IconType::save)); // ImGui::SameLine(); // if (ImGui::Selectable((_L("Save used as") + dots).ToUTF8().data())) { // GUI::FileType file_type = FT_SVG; // wxString wildcard = file_wildcards(file_type); // wxString dlg_title = _L("Export SVG file:"); // wxString dlg_dir = from_u8(wxGetApp().app_config->get_last_dir()); // const EmbossShape::SvgFile& svg = m_volume_shape.svg_file; // wxString dlg_file = from_u8(get_file_name(((!svg.path.empty()) ? svg.path : svg.path_in_3mf))) + ".svg"; // wxFileDialog dlg(nullptr, dlg_title, dlg_dir, dlg_file, wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT); // if (dlg.ShowModal() == wxID_OK ){ // wxString out_path = dlg.GetPath(); // std::string path{out_path.c_str()}; // Slic3r::save(*m_volume_shape.svg_file.image, path); // } //} else if (ImGui::IsItemHovered()) { // ImGui::SetTooltip("%s", _u8L("Save only used path as '.svg' file").c_str()); //} ImGui::PopStyleVar(1); } ImGuiWrapper::pop_combo_style(); if (!tooltip.empty()) m_imgui->tooltip(tooltip, m_gui_cfg->max_tooltip_width); if (file_changed) { float scale = get_scale_for_tolerance(); double tes_tol = get_tesselation_tolerance(scale); EmbossShape es_ = select_shape(m_volume_shape.svg_file->path, tes_tol); m_volume_shape.svg_file = std::move(es_.svg_file); m_volume_shape.shapes_with_ids = std::move(es_.shapes_with_ids); m_volume_shape.final_shape = {}; // clear cache m_shape_warnings = create_shape_warnings(m_volume_shape, scale); init_texture(m_texture, m_volume_shape.shapes_with_ids, m_gui_cfg->texture_max_size_px, m_shape_warnings); process_job(); } } void GLGizmoSVG::draw_depth() { ImGui::AlignTextToFramePadding(); ImGuiWrapper::text(m_gui_cfg->translations.depth); ImGui::SameLine(m_gui_cfg->input_offset); ImGui::SetNextItemWidth(m_gui_cfg->input_width); bool use_inch = wxGetApp().app_config->get_bool("use_inches"); double & value = m_volume_shape.projection.depth; constexpr double step = 1.; constexpr double step_fast = 10.; std::optional result_scale; const char * size_format = "%.2f mm"; double input = value; if (use_inch) { size_format = "%.2f in"; // input in inches input *= GizmoObjectManipulation::mm_to_in * m_scale_depth.value_or(1.f); result_scale = GizmoObjectManipulation::in_to_mm / m_scale_depth.value_or(1.f); } else if (m_scale_depth.has_value()) { // scale input input *= (*m_scale_depth); result_scale = 1. / (*m_scale_depth); } if (ImGui::InputDouble("##depth", &input, step, step_fast, size_format)) { if (result_scale.has_value()) input *= (*result_scale); apply(input, limits.depth); if (!is_approx(input, value, 1e-4)) { value = input; process_job(); } } else if (ImGui::IsItemHovered()) m_imgui->tooltip(_L("Size in emboss direction."), m_gui_cfg->max_tooltip_width); } void GLGizmoSVG::draw_size() { ImGui::AlignTextToFramePadding(); ImGuiWrapper::text(m_gui_cfg->translations.size); if (ImGui::IsItemHovered()) { size_t count_points = 0; for (const auto &s : m_volume_shape.shapes_with_ids) count_points += Slic3r::count_points(s.expoly); // TRN: The placeholder contains a number. m_imgui->tooltip(GUI::format_wxstr(_L("Scale also changes amount of curve samples (%1%)"), count_points), m_gui_cfg->max_tooltip_width); } bool use_inch = wxGetApp().app_config->get_bool("use_inches"); Point size = m_shape_bb.size(); double width = size.x() * m_volume_shape.scale * m_scale_width.value_or(1.f); float ui_size_max = m_origin_shape_bb.size().x() * m_volume_shape.scale * 2; if (use_inch) { width *= GizmoObjectManipulation::mm_to_in; //ui_size_max = 2 * width; } double height = size.y() * m_volume_shape.scale * m_scale_height.value_or(1.f); if (use_inch) { height *= GizmoObjectManipulation::mm_to_in; } const auto is_valid_scale_ratio = [limit = &limits.relative_scale_ratio](double ratio) { if (std::fabs(ratio - 1.) < limit->min) return false; // too small ratio --> without effect if (ratio > limit->max) return false; if (ratio < 1e-4) return false; // negative scale is not allowed return true; }; std::optional new_relative_scale; bool make_snap = false; const MinMax &minmax = use_inch ? limits.ui_size_in : limits.ui_size; if (m_keep_ratio) { std::stringstream ss; ss << std::setprecision(2) << std::fixed << width << " x " << std::setprecision(2) << std::fixed << height << (use_inch ? "in" : "mm"); ImGui::SameLine(m_gui_cfg->input_offset); ImGui::SetNextItemWidth(m_gui_cfg->input_width); // convert to float for slider float width_f = static_cast(width); if (m_imgui->slider_float("##width_size_slider", &width_f, minmax.min, ui_size_max, ss.str().c_str(), 1.f, false, _L("set width and height keep ratio with width"))) { double width_ratio = width_f / width; if (is_valid_scale_ratio(width_ratio)) { m_scale_width = m_scale_width.value_or(1.f) * width_ratio; m_scale_height = m_scale_height.value_or(1.f) * width_ratio; new_relative_scale = Vec3d(width_ratio, width_ratio, 1.); } } if (m_imgui->get_last_slider_status().deactivated_after_edit) make_snap = true; // only last change of slider make snap } else { ImGuiInputTextFlags flags = 0; float space = m_gui_cfg->icon_width / 2; float input_width = m_gui_cfg->input_width / 2 - space / 2; float second_offset = m_gui_cfg->input_offset + input_width + space; const char *size_format = (use_inch) ? "%.2f in" : "%.1f mm"; double step = -1.0; double fast_step = -1.0; ImGui::SameLine(m_gui_cfg->input_offset); ImGui::SetNextItemWidth(input_width); double prev_width = width; if (ImGui::InputDouble("##width", &width, step, fast_step, size_format, flags)) { limit_value(width, (double) minmax.min, (double) MAX_NUM); double width_ratio = width / prev_width; if (is_valid_scale_ratio(width_ratio)) { m_scale_width = m_scale_width.value_or(1.f) * width_ratio; new_relative_scale = Vec3d(width_ratio, 1., 1.); make_snap = true; } } if (ImGui::IsItemHovered()) m_imgui->tooltip(_L("Width of SVG."), m_gui_cfg->max_tooltip_width); ImGui::SameLine(second_offset); ImGui::SetNextItemWidth(input_width); double prev_height = height; if (ImGui::InputDouble("##height", &height, step, fast_step, size_format, flags)) { limit_value(height, (double) minmax.min, (double) MAX_NUM); double height_ratio = height / prev_height; if (is_valid_scale_ratio(height_ratio)) { m_scale_height = m_scale_height.value_or(1.f) * height_ratio; new_relative_scale = Vec3d(1., height_ratio, 1.); make_snap = true; } } if (ImGui::IsItemHovered()) m_imgui->tooltip(_L("Height of SVG."), m_gui_cfg->max_tooltip_width); } // Lock on ratio m_keep_ratio ImGui::SameLine(m_gui_cfg->lock_offset); const IconManager::Icon &icon = get_icon(m_icons, m_keep_ratio ? IconType::lock : IconType::unlock, IconState::activable); const IconManager::Icon &icon_hover = get_icon(m_icons, m_keep_ratio ? IconType::lock : IconType::unlock, IconState::hovered); if (button(icon, icon_hover, icon)) m_keep_ratio = !m_keep_ratio; if (ImGui::IsItemHovered()) m_imgui->tooltip(_L("Lock/unlock the aspect ratio of the SVG."), m_gui_cfg->max_tooltip_width); // reset button bool can_reset = m_scale_width.has_value() || m_scale_height.has_value(); if (can_reset) { if (reset_button(m_icons)) { new_relative_scale = Vec3d(1. / m_scale_width.value_or(1.f), 1. / m_scale_height.value_or(1.f), 1.); make_snap = true; } else if (ImGui::IsItemHovered()) m_imgui->tooltip(_L("Reset scale"), m_gui_cfg->max_tooltip_width); } if (new_relative_scale.has_value()) { Selection &selection = m_parent.get_selection(); selection.setup_cache(); auto selection_scale_fnc = [&selection, rel_scale = *new_relative_scale]() { selection.scale(rel_scale, get_drag_transformation_type(selection)); }; selection_transform(selection, selection_scale_fnc); std::string snap_name; // Empty mean do not store on undo/redo stack m_parent.do_scale(snap_name); wxGetApp().obj_manipul()->set_dirty(); // should be the almost same calculate_scale(); const NSVGimage *img = m_volume_shape.svg_file->image.get(); assert(img != NULL); if (img != NULL) { NSVGLineParams params{get_tesselation_tolerance(get_scale_for_tolerance())}; m_volume_shape.shapes_with_ids = create_shape_with_ids(*img, params); m_volume_shape.final_shape = {}; // reset cache for final shape if (!make_snap) // Be carefull: Last change may be without change of scale process_job(false); } } if (make_snap) process_job(); // make undo/redo snap-shot} } void GLGizmoSVG::draw_use_surface() { m_imgui->disabled_begin(!m_can_use_surface); ScopeGuard sc([imgui = m_imgui]() { imgui->disabled_end(); }); ImGui::AlignTextToFramePadding(); ImGuiWrapper::text(m_gui_cfg->translations.use_surface); ImGui::SameLine(m_gui_cfg->input_offset); if (Emboss::UpdateSurfaceVolumeJob::is_use_surfae_error) { Emboss::UpdateSurfaceVolumeJob::is_use_surfae_error = false; m_volume_shape.projection.use_surface = false; } if (m_imgui->bbl_checkbox("##useSurface", m_volume_shape.projection.use_surface)) { process_job(); } } void GLGizmoSVG::draw_distance() { const EmbossProjection &projection = m_volume->emboss_shape->projection; bool use_surface = projection.use_surface; bool allowe_surface_distance = !use_surface && !m_volume->is_the_only_one_part(); float prev_distance = m_distance.value_or(.0f); float min_distance = static_cast(-2 * projection.depth); float max_distance = static_cast(2 * projection.depth); m_imgui->disabled_begin(!allowe_surface_distance); ScopeGuard sg([imgui = m_imgui]() { imgui->disabled_end(); }); ImGui::AlignTextToFramePadding(); ImGuiWrapper::text(m_gui_cfg->translations.distance); ImGui::SameLine(m_gui_cfg->input_offset); ImGui::SetNextItemWidth(m_gui_cfg->input_width); bool use_inch = wxGetApp().app_config->get_bool("use_inches"); const wxString move_tooltip = _L("Distance of the center of the SVG to the model surface."); bool is_moved = false; if (use_inch) { std::optional distance_inch; if (m_distance.has_value()) distance_inch = (*m_distance * GizmoObjectManipulation::mm_to_in); min_distance = static_cast(min_distance * GizmoObjectManipulation::mm_to_in); max_distance = static_cast(max_distance * GizmoObjectManipulation::mm_to_in); if (m_imgui->slider_optional_float("##distance", m_distance, min_distance, max_distance, "%.3f in", 1.f, false, move_tooltip)) { limit_value(*m_distance, min_distance, max_distance); if (distance_inch.has_value()) { m_distance = *distance_inch * GizmoObjectManipulation::in_to_mm; } else { m_distance.reset(); } is_moved = true; } } else { if (m_imgui->slider_optional_float("##distance", m_distance, min_distance, max_distance, "%.2f mm", 1.f, false, move_tooltip)) { limit_value(*m_distance, min_distance, max_distance); is_moved = true; } } bool is_stop_sliding = m_imgui->get_last_slider_status().deactivated_after_edit; bool is_reseted = false; if (m_distance.has_value()) { if (reset_button(m_icons)) { m_distance.reset(); is_reseted = true; } else if (ImGui::IsItemHovered()) m_imgui->tooltip(_L("Reset distance"), m_gui_cfg->max_tooltip_width); } if (is_moved || is_reseted) do_local_z_move(m_parent.get_selection(), m_distance.value_or(.0f) - prev_distance); if (is_stop_sliding || is_reseted) m_parent.do_move(move_snapshot_name); } void GLGizmoSVG::draw_rotation() { bool allow_rotation = true; if (!m_volume->is_the_only_one_part()) { const EmbossProjection &projection = m_volume->emboss_shape->projection; bool use_surface = projection.use_surface; allow_rotation = !use_surface; } m_imgui->disabled_begin(!allow_rotation); ImGui::AlignTextToFramePadding(); ImGuiWrapper::text(m_gui_cfg->translations.rotation); ImGui::SameLine(m_gui_cfg->input_offset); ImGui::SetNextItemWidth(m_gui_cfg->input_width); // slider for Clock-wise angle in degress // stored angle is optional CCW and in radians // Convert stored value to degress // minus create clock-wise roation from CCW float angle = m_angle.value_or(0.f); float angle_deg = static_cast(-angle * 180 / M_PI); if (m_imgui->slider_float("##angle", &angle_deg, limits.angle.min, limits.angle.max, u8"%.2f °", 1.f, false, _L("Rotate text Clock-wise."))) { // convert back to radians and CCW double angle_rad = -angle_deg * M_PI / 180.0; Geometry::to_range_pi_pi(angle_rad); double diff_angle = angle_rad - angle; do_local_z_rotate(m_parent.get_selection(), diff_angle); // calc angle after rotation m_angle = calc_angle(m_parent.get_selection()); // recalculate for surface cut if (m_volume->emboss_shape->projection.use_surface) process_job(); } bool is_stop_sliding = m_imgui->get_last_slider_status().deactivated_after_edit; // Reset button bool is_reseted = false; if (m_angle.has_value()) { if (reset_button(m_icons)) { do_local_z_rotate(m_parent.get_selection(), -(*m_angle)); m_angle.reset(); // recalculate for surface cut if (m_volume->emboss_shape->projection.use_surface) process_job(); is_reseted = true; } else if (ImGui::IsItemHovered()) m_imgui->tooltip(_L("Reset rotation"), m_gui_cfg->max_tooltip_width); } // Apply rotation on model (backend) if (is_stop_sliding || is_reseted) m_parent.do_rotate(rotation_snapshot_name); // Keep up - lock button icon if (!m_volume->is_the_only_one_part()) { ImGui::SameLine(m_gui_cfg->lock_offset); const IconManager::Icon &icon = get_icon(m_icons, m_keep_up ? IconType::lock : IconType::unlock, IconState::activable); const IconManager::Icon &icon_hover = get_icon(m_icons, m_keep_up ? IconType::lock : IconType::unlock, IconState::hovered); if (button(icon, icon_hover, icon)) m_keep_up = !m_keep_up; if (ImGui::IsItemHovered()) m_imgui->tooltip(_L("Lock/unlock rotation angle when dragging above the surface."), m_gui_cfg->max_tooltip_width); } m_imgui->disabled_end(); } void GLGizmoSVG::draw_mirroring() { ImGui::AlignTextToFramePadding(); ImGui::Text("%s", m_gui_cfg->translations.mirror.c_str()); ImGui::SameLine(m_gui_cfg->input_offset); Axis axis = Axis::UNKNOWN_AXIS; if (draw_clickable(m_icons, IconType::reflection_x)) { axis = Axis::X; } else if (ImGui::IsItemHovered()) { m_imgui->tooltip(_L("Mirror vertically"), m_gui_cfg->max_tooltip_width); } ImGui::SameLine(); if (draw_clickable(m_icons, IconType::reflection_y)) { axis = Axis::Y; } else if (ImGui::IsItemHovered()) { m_imgui->tooltip(_L("Mirror horizontally"), m_gui_cfg->max_tooltip_width); } if (axis != Axis::UNKNOWN_AXIS) { Selection &selection = m_parent.get_selection(); selection.setup_cache(); auto selection_mirror_fnc = [&selection, &axis]() { selection.mirror(axis, get_drag_transformation_type(selection)); }; selection_transform(selection, selection_mirror_fnc); m_parent.do_mirror(L("Set Mirror")); // Mirror is ignoring keep up !! if (m_keep_up) m_angle = calc_angle(selection); volume_transformation_changed(); if (m_volume_shape.projection.use_surface) process_job(); } } void GLGizmoSVG::draw_face_the_camera() { if (ImGui::Button(_u8L("Face the camera").c_str())) { const Camera &cam = wxGetApp().plater()->get_camera(); auto wanted_up_limit = (m_keep_up) ? std::optional(UP_LIMIT) : std::optional{}; if (face_selected_volume_to_camera(cam, m_parent, wanted_up_limit)) volume_transformation_changed(); } } void GLGizmoSVG::draw_model_type() { ImGui::AlignTextToFramePadding(); bool is_last_solid_part = m_volume->is_the_only_one_part(); std::string title = _u8L("Operation"); if (is_last_solid_part) { ImVec4 color{.5f, .5f, .5f, 1.f}; m_imgui->text_colored(color, title.c_str()); } else { ImGui::Text("%s", title.c_str()); } std::optional new_type; ModelVolumeType modifier = ModelVolumeType::PARAMETER_MODIFIER; ModelVolumeType negative = ModelVolumeType::NEGATIVE_VOLUME; ModelVolumeType part = ModelVolumeType::MODEL_PART; ModelVolumeType type = m_volume->type(); ImGui::SameLine(m_gui_cfg->input_offset); // TRN EmbossOperation ImGuiWrapper::push_radio_style(); if (ImGui::RadioButton(_u8L("Join").c_str(), type == part)) new_type = part; else if (ImGui::IsItemHovered()) m_imgui->tooltip(_L("Click to change text into object part."), m_gui_cfg->max_tooltip_width); ImGui::SameLine(); auto last_solid_part_hint = _L("You can't change a type of the last solid part of the object."); if (ImGui::RadioButton(_CTX_utf8(L_CONTEXT("Cut", "EmbossOperation"), "EmbossOperation").c_str(), type == negative)) new_type = negative; else if (ImGui::IsItemHovered()) { if (is_last_solid_part) m_imgui->tooltip(last_solid_part_hint, m_gui_cfg->max_tooltip_width); else if (type != negative) m_imgui->tooltip(_L("Click to change part type into negative volume."), m_gui_cfg->max_tooltip_width); } // In simple mode are not modifiers if (wxGetApp().plater()->printer_technology() != ptSLA && wxGetApp().get_mode() != ConfigOptionMode::comSimple) { ImGui::SameLine(); if (ImGui::RadioButton(_u8L("Modifier").c_str(), type == modifier)) new_type = modifier; else if (ImGui::IsItemHovered()) { if (is_last_solid_part) m_imgui->tooltip(last_solid_part_hint, m_gui_cfg->max_tooltip_width); else if (type != modifier) m_imgui->tooltip(_L("Click to change part type into modifier."), m_gui_cfg->max_tooltip_width); } } ImGuiWrapper::pop_radio_style(); if (m_volume != nullptr && new_type.has_value() && !is_last_solid_part) { GUI_App &app = wxGetApp(); Plater * plater = app.plater(); // TRN: This is the name of the action that shows in undo/redo stack (changing part type from SVG to something else). Plater::TakeSnapshot snapshot(plater, _u8L("Change SVG Type"), UndoRedo::SnapshotType::GizmoAction); m_volume->set_type(*new_type); bool is_volume_move_inside = (type == part); bool is_volume_move_outside = (*new_type == part); // Update volume position when switch (from part) or (into part) if ((is_volume_move_inside || is_volume_move_outside)) process_job(); // inspiration in ObjectList::change_part_type() // how to view correct side panel with objects ObjectList * obj_list = app.obj_list(); wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection(obj_list->get_selected_obj_idx(), [volume = m_volume](const ModelVolume *vol) { return vol == volume; }); if (!sel.IsEmpty()) obj_list->select_item(sel.front()); // NOTE: on linux, function reorder_volumes_and_get_selection call GLCanvas3D::reload_scene(refresh_immediately = false) // which discard m_volume pointer and set it to nullptr also selection is cleared so gizmo is automaticaly closed auto &mng = m_parent.get_gizmos_manager(); if (mng.get_current_type() != GLGizmosManager::Svg) mng.open_gizmo(GLGizmosManager::Svg); // TODO: select volume back - Ask @Sasa } } void GLGizmoSVG::volume_transformation_changed() { if (m_volume == nullptr || !m_volume->emboss_shape.has_value()) { assert(false); return; } if (!m_keep_up) { // update current style m_angle = calc_angle(m_parent.get_selection()); } else { // angle should be the same assert(is_approx(m_angle, calc_angle(m_parent.get_selection()))); } // Update surface by new position if (m_volume->emboss_shape->projection.use_surface) { process_job(); } else { // inform slicing process that model changed // SLA supports, processing // ensure on bed wxGetApp().plater()->changed_object(*m_volume->get_object()); } // Show correct value of height & depth inside of inputs calculate_scale(); } bool GLGizmoSVG::on_mouse_for_rotation(const wxMouseEvent &mouse_event) { if (mouse_event.Moving()) return false; bool used = use_grabbers(mouse_event); if (!m_dragging) return used; if (mouse_event.Dragging()) dragging_rotate_gizmo(m_rotate_gizmo.get_angle(), m_angle, m_rotate_start_angle, m_parent.get_selection()); return used; } bool GLGizmoSVG::on_mouse_for_translate(const wxMouseEvent &mouse_event) { // exist selected volume? if (m_volume == nullptr) return false; auto up_limit = m_keep_up ? std::optional(UP_LIMIT) : std::optional{}; const Camera &camera = wxGetApp().plater()->get_camera(); bool was_dragging = m_surface_drag.has_value(); bool res = on_mouse_surface_drag(mouse_event, camera, m_surface_drag, m_parent, m_raycast_manager, up_limit); bool is_dragging = m_surface_drag.has_value(); // End with surface dragging? if (was_dragging && !is_dragging) { // Update surface by new position if (m_volume->emboss_shape->projection.use_surface) process_job(); // TODO: Remove it when it will be stable // Distance should not change during dragging const GLVolume *gl_volume = m_parent.get_selection().get_first_volume(); m_distance = calc_distance(*gl_volume, m_raycast_manager, m_parent); // Show correct value of height & depth inside of inputs calculate_scale(); } // Start with dragging else if (!was_dragging && is_dragging) { // Cancel job to prevent interuption of dragging (duplicit result) //if (m_job_cancel != nullptr) // m_job_cancel->store(true); } // during drag else if (was_dragging && is_dragging) { // update scale of selected volume --> should be approx the same calculate_scale(); // Recalculate angle for GUI if (!m_keep_up) m_angle = calc_angle(m_parent.get_selection()); } return res; } void GLGizmoSVG::register_single_mesh_pick() { std::map>().swap(m_mesh_raycaster_map); Selection & selection = m_parent.get_selection(); int object_idx; ModelObject * mo = selection.get_selected_single_object(object_idx); std::vector volume_idxs = selection.get_volume_idxs_from_object(object_idx); if (volume_idxs.empty()) { m_raycast_condition.clear(); m_raycast_manager.clear(); return; } for (unsigned int idx : volume_idxs) { GLVolume *v = const_cast(selection.get_volume(idx)); if (v == m_svg_volume) { continue; } auto world_tran = v->get_instance_transformation() * v->get_volume_transformation(); auto mesh = const_cast(v->ori_mesh); if (m_mesh_raycaster_map.find(v) != m_mesh_raycaster_map.end()) { m_mesh_raycaster_map[v]->world_tran.set_from_transform(world_tran.get_matrix()); } else { m_mesh_raycaster_map[v] = std::make_shared(mesh, -1); m_mesh_raycaster_map[v]->world_tran.set_from_transform(world_tran.get_matrix()); } } const ModelInstance * mi = mo->instances[selection.get_instance_idx()]; if (m_svg_volume) { const ModelVolume *svg_mv = get_model_volume(*m_svg_volume, *mo); m_raycast_condition = create_condition(mo->volumes, svg_mv->id()); } else { m_raycast_condition = create_condition(mo->volumes, -1); } RaycastManager::Meshes meshes; // = create_meshes(input.canvas, cond); meshes.reserve(m_mesh_raycaster_map.size()); for (auto iter : m_mesh_raycaster_map) { auto gl_volume = iter.first; const ModelVolume *mv = get_model_volume(*gl_volume, *mo); size_t id = mv->id().id; auto mesh = std::make_unique(iter.second->mesh_raycaster->get_aabb_mesh()); meshes.emplace_back(std::make_pair(id, std::move(mesh))); } m_raycast_manager.actualize(*mi, &m_raycast_condition, &meshes); // input.raycaster.actualize(*instance, &cond, &meshes); } bool GLGizmoSVG::gizmo_event(SLAGizmoEventType action, const Vec2d &mouse_position, bool shift_down, bool alt_down, bool control_down) { if (m_volume && m_volume->is_the_only_one_part()){ return false; } const Selection & selection = m_parent.get_selection(); if (action == SLAGizmoEventType::Moving) { } else if (action == SLAGizmoEventType::LeftDown) { if (!selection.is_empty() && get_hover_id() != -1) { //start_dragging(); return true; } } return true; } BoundingBoxf3 GLGizmoSVG::get_bounding_box() const { BoundingBoxf3 t_aabb; t_aabb.reset(); // m_rotate_gizmo aabb bool is_rotate_by_grabbers = m_dragging; bool is_surface_dragging = m_surface_drag.has_value(); bool is_parent_dragging = m_parent.is_mouse_dragging(); if (is_rotate_by_grabbers || (!is_surface_dragging && !is_parent_dragging)) { if (m_hover_id != c_move_cube_id || !m_dragging) { auto t_totate_gizmo_aabb = m_rotate_gizmo.get_bounding_box(); t_aabb.merge(t_totate_gizmo_aabb); t_aabb.defined = true; } } // end m_rotate_gizmo aabb return t_aabb; } void GLGizmoSVG::update_single_mesh_pick(GLVolume *v) { if (m_mesh_raycaster_map.find(v) != m_mesh_raycaster_map.end()) { auto world_tran = v->get_instance_transformation() * v->get_volume_transformation(); m_mesh_raycaster_map[v]->world_tran.set_from_transform(world_tran.get_matrix()); } } }} // namespace Slic3r