2227 lines
87 KiB
C++
2227 lines
87 KiB
C++
// 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 <imgui/imgui_internal.h>
|
|
#include "nanosvg/nanosvg.h" // load SVG file
|
|
|
|
#include <wx/display.h> // detection of change DPI
|
|
#include <boost/log/trivial.hpp>
|
|
|
|
|
|
#include <GL/glew.h>
|
|
|
|
#include <algorithm>
|
|
#include <wx/utils.h>
|
|
#include <chrono> // measure enumeration of fonts
|
|
#include <sstream> // save for svg
|
|
#include <array>
|
|
#include <fstream>
|
|
|
|
#include "slic3r/GUI/BitmapCache.hpp"
|
|
#include "libslic3r/AABBTreeLines.hpp" // aabb lines for draw filled expolygon
|
|
#include <libslic3r/SLA/ReprojectPointsOnMesh.hpp>
|
|
#include <libslic3r/CutSurface.hpp> // 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<float, 4> SVG_Move_GrabberColor = {1.0, 1.0, 0.0, 1.0};
|
|
const std::array<float, 4> 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<double> depth{0.01, 1e4}; // in mm
|
|
MinMax<float> size{0.01f, 1e4f}; // in mm (width + height)
|
|
MinMax<float> ui_size{5.f, 100.f}; // in mm (width + height) - only slider values
|
|
MinMax<float> ui_size_in{.1f, 4.f}; // in inches (width + height) - only slider values
|
|
MinMax<double> relative_scale_ratio{1e-5, 1e4}; // change size
|
|
// distance text object from surface
|
|
MinMax<float> 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<std::atomic<bool>> &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<std::atomic<bool>>(false);
|
|
|
|
std::string name = volume_name(shape);
|
|
|
|
auto result = std::make_unique<Emboss::DataBase>(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<unsigned char>(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<std::string> 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<size_t>(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<float, 4> 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<const GuiCfg>(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<Transform3d> &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<float> &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<void(int x, int y, float brightess)> &plot)
|
|
{
|
|
auto ipart = [](float x) -> int { return static_cast<int>(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<unsigned int N> // N .. count of channels per pixel
|
|
void draw_side_outline(const ExPolygons &shape, const std::array<unsigned char, N> &color, std::vector<unsigned char> &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<size_t>(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<unsigned char>(std::round(brightess * 255));
|
|
} else if (alpha != 255) {
|
|
alpha = static_cast<unsigned char>(std::min(255, int(alpha) + static_cast<int>(std::round(brightess * 255))));
|
|
}
|
|
};
|
|
|
|
BoundingBox bb_unscaled = get_extents(shape);
|
|
Linesf lines = to_linesf(shape);
|
|
BoundingBoxf bb(bb_unscaled.min.cast<double>(), bb_unscaled.max.cast<double>());
|
|
|
|
// 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw filled ExPolygon into data
|
|
/// line by line inspired by: http://alienryderflex.com/polygon_fill/
|
|
/// </summary>
|
|
/// <typeparam name="N">Count channels for one pixel(RGBA = 4)</typeparam>
|
|
/// <param name="shape">Shape to draw</param>
|
|
/// <param name="color">Color of shape contain count of channels(N)</param>
|
|
/// <param name="data">Image(2d) stored in 1d array</param>
|
|
/// <param name="data_width">Count of pixel on one line(size in data = N x data_width)</param>
|
|
/// <param name="scale">Shape scale for conversion to pixels</param>
|
|
template<unsigned int N> // N .. count of channels per pixel
|
|
void draw_filled(const ExPolygons &shape, const std::array<unsigned char, N> &color, std::vector<unsigned char> &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<double>(), bb_unscaled.max.cast<double>());
|
|
|
|
// 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<int>(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<unsigned char>(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<unsigned char>(std::min(255, int(alpha) + static_cast<int>(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<int>(std::round(bb.max.y())));
|
|
for (int y = std::max(0, static_cast<int>(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<Vec2d, size_t>;
|
|
using Intersections = std::vector<Intersection>;
|
|
// sorted .. false
|
|
// <false, Vec2d, Linef, decltype(tree)>
|
|
Intersections intersections = Slic3r::AABBTreeLines::get_intersections_with_line<false, Vec2d, Linef>(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<int>(data_width - 1), static_cast<int>(std::round(p2.x())));
|
|
for (int x = std::max(0, static_cast<int>(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<std::string> &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<unsigned>(std::ceil(bb_height * scale));
|
|
} else {
|
|
scale = max_size_px / bb_height;
|
|
texture.width = static_cast<unsigned>(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<unsigned char> 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<size_t>(texture.width);
|
|
unsigned char alpha = 255; // without transparency
|
|
std::array<unsigned char, 4> color_shape{201, 201, 201, alpha}; // from degin by @JosefZachar
|
|
std::array<unsigned char, 4> color_error{237, 28, 36, alpha}; // from icon: resources/icons/flag_red.svg
|
|
std::array<unsigned char, 4> 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)); }
|
|
|
|
/// <summary>
|
|
/// Create warnings about shape
|
|
/// </summary>
|
|
/// <param name="image">Input svg loaded to shapes</param>
|
|
/// <returns>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<std::string> GLGizmoSVG::create_shape_warnings(const EmbossShape &shape, float scale)
|
|
{
|
|
const std::shared_ptr<NSVGimage> &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<std::string> result;
|
|
auto add_warning = [&result, &image](size_t index, const std::string &message) {
|
|
if (result.empty()) result = std::vector<std::string>(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<std::atomic<bool>>(false);
|
|
|
|
EmbossShape shape = m_volume_shape; // copy
|
|
auto base = std::make_unique<Emboss::DataBase>(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<intptr_t>(m_texture.id);
|
|
unsigned window_width = static_cast<unsigned>(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<float> 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<double> 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<Vec3d> new_relative_scale;
|
|
bool make_snap = false;
|
|
const MinMax<float> &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<float>(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<double>(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<double>(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<float>(-2 * projection.depth);
|
|
float max_distance = static_cast<float>(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<float> distance_inch;
|
|
if (m_distance.has_value()) distance_inch = (*m_distance * GizmoObjectManipulation::mm_to_in);
|
|
min_distance = static_cast<float>(min_distance * GizmoObjectManipulation::mm_to_in);
|
|
max_distance = static_cast<float>(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<float>(*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<float>(*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<float>(-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<double>(UP_LIMIT) : std::optional<double>{};
|
|
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<ModelVolumeType> 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<double>(UP_LIMIT) : std::optional<double>{};
|
|
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<GLVolume *, std::shared_ptr<PickRaycaster>>().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<unsigned int> 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<GLVolume *>(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<TriangleMesh *>(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<PickRaycaster>(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<sla::IndexedMesh>(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
|