791 lines
29 KiB
C++
791 lines
29 KiB
C++
#include "EmbossJob.hpp"
|
|
|
|
#include <stdexcept>
|
|
|
|
#include <libslic3r/Model.hpp>
|
|
#include <libslic3r/Format/OBJ.hpp> // load_obj for default mesh
|
|
#include <libslic3r/CutSurface.hpp> // use surface cuts
|
|
|
|
#include "slic3r/GUI/Plater.hpp"
|
|
#include "slic3r/GUI/NotificationManager.hpp"
|
|
#include "slic3r/GUI/GLCanvas3D.hpp"
|
|
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
|
#include "slic3r/GUI/MainFrame.hpp"
|
|
#include "slic3r/GUI/GUI.hpp"
|
|
#include "slic3r/GUI/GUI_App.hpp"
|
|
#include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp"
|
|
#include "slic3r/GUI/CameraUtils.hpp"
|
|
#include "slic3r/GUI/format.hpp"
|
|
#include "slic3r/Utils/UndoRedo.hpp"
|
|
|
|
using namespace Slic3r;
|
|
using namespace Slic3r::Emboss;
|
|
using namespace Slic3r::GUI;
|
|
using namespace Slic3r::GUI::Emboss;
|
|
|
|
// private namespace
|
|
namespace priv{
|
|
// create sure that emboss object is bigger than source object [in mm]
|
|
constexpr float safe_extension = 1.0f;
|
|
|
|
/// <summary>
|
|
/// Assert check of inputs data
|
|
/// </summary>
|
|
/// <param name="input"></param>
|
|
/// <returns></returns>
|
|
bool check(const DataBase &input, bool check_fontfile = true, bool use_surface = false);
|
|
bool check(const DataCreateVolume &input, bool is_main_thread = false);
|
|
bool check(const DataCreateObject &input);
|
|
bool check(const DataUpdate &input, bool is_main_thread = false, bool use_surface = false);
|
|
bool check(const CreateSurfaceVolumeData &input, bool is_main_thread = false);
|
|
bool check(const UpdateSurfaceVolumeData &input, bool is_main_thread = false);
|
|
|
|
// <summary>
|
|
/// Try to create mesh from text
|
|
/// </summary>
|
|
/// <param name="input">Text to convert on mesh
|
|
/// + Shape of characters + Property of font</param>
|
|
/// <param name="font">Font file with cache
|
|
/// NOTE: Cache glyphs is changed</param>
|
|
/// <param name="was_canceled">To check if process was canceled</param>
|
|
/// <returns>Triangle mesh model</returns>
|
|
template<typename Fnc> static TriangleMesh try_create_mesh(const DataBase &input, FontFileWithCache &font, Fnc was_canceled);
|
|
template<typename Fnc> static TriangleMesh create_mesh(DataBase &input, Fnc was_canceled, Job::Ctl &ctl);
|
|
|
|
/// <summary>
|
|
/// Create default mesh for embossed text
|
|
/// </summary>
|
|
/// <returns>Not empty model(index trinagle set - its)</returns>
|
|
static TriangleMesh create_default_mesh();
|
|
|
|
/// <summary>
|
|
/// Must be called on main thread
|
|
/// </summary>
|
|
/// <param name="mesh">New mesh data</param>
|
|
/// <param name="data">Text configuration, ...</param>
|
|
static void update_volume(TriangleMesh &&mesh, const DataUpdate &data);
|
|
|
|
/// <summary>
|
|
/// Add new volume to object
|
|
/// </summary>
|
|
/// <param name="mesh">triangles of new volume</param>
|
|
/// <param name="object_id">Object where to add volume</param>
|
|
/// <param name="type">Type of new volume</param>
|
|
/// <param name="trmat">Transformation of volume inside of object</param>
|
|
/// <param name="data">Text configuration and New VolumeName</param>
|
|
static void create_volume(TriangleMesh &&mesh, const ObjectID& object_id,
|
|
const ModelVolumeType type, const Transform3d trmat, const DataBase &data);
|
|
|
|
/// <summary>
|
|
/// Select Volume from objects
|
|
/// </summary>
|
|
/// <param name="objects">All objects in scene</param>
|
|
/// <param name="volume_id">Identifier of volume in object</param>
|
|
/// <returns>Pointer to volume when exist otherwise nullptr</returns>
|
|
static ModelVolume *get_volume(ModelObjectPtrs &objects, const ObjectID &volume_id);
|
|
|
|
/// <summary>
|
|
/// Create projection for cut surface from mesh
|
|
/// </summary>
|
|
/// <param name="tr">Volume transformation in object</param>
|
|
/// <param name="shape_scale">Convert shape to milimeters</param>
|
|
/// <param name="z_range">Bounding box 3d of model volume for projection ranges</param>
|
|
/// <returns>Orthogonal cut_projection</returns>
|
|
static OrthoProject create_projection_for_cut(Transform3d tr, double shape_scale, const std::pair<float, float> &z_range);
|
|
|
|
/// <summary>
|
|
/// Create tranformation for emboss Cutted surface
|
|
/// </summary>
|
|
/// <param name="is_outside">True .. raise, False .. engrave</param>
|
|
/// <param name="emboss">Depth of embossing</param>
|
|
/// <param name="tr">Text voliume transformation inside object</param>
|
|
/// <param name="cut">Cutted surface from model</param>
|
|
/// <returns>Projection</returns>
|
|
static OrthoProject3d create_emboss_projection(bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut);
|
|
|
|
/// <summary>
|
|
/// Cut surface into triangle mesh
|
|
/// </summary>
|
|
/// <param name="input1">(can't be const - cache of font)</param>
|
|
/// <param name="input2">SurfaceVolume data</param>
|
|
/// <param name="was_canceled">Check to interupt execution</param>
|
|
/// <returns>Extruded object from cuted surace</returns>
|
|
static TriangleMesh cut_surface(/*const*/ DataBase &input1, const SurfaceVolumeData &input2, std::function<bool()> was_canceled);
|
|
|
|
static void create_message(const std::string &message); // only in finalize
|
|
static bool process(std::exception_ptr &eptr);
|
|
|
|
class JobException : public std::runtime_error {
|
|
public: JobException(const char* message):runtime_error(message){}};
|
|
|
|
}// namespace priv
|
|
|
|
/////////////////
|
|
/// Create Volume
|
|
CreateVolumeJob::CreateVolumeJob(DataCreateVolume &&input)
|
|
: m_input(std::move(input))
|
|
{
|
|
assert(priv::check(m_input, true));
|
|
}
|
|
|
|
void CreateVolumeJob::process(Ctl &ctl) {
|
|
if (!priv::check(m_input)) throw std::runtime_error("Bad input data for EmbossCreateVolumeJob.");
|
|
auto was_canceled = [&ctl]()->bool { return ctl.was_canceled(); };
|
|
m_result = priv::create_mesh(m_input, was_canceled, ctl);
|
|
// center result
|
|
Vec3f c = m_result.bounding_box().center().cast<float>();
|
|
if (!c.isApprox(Vec3f::Zero())) m_result.translate(-c);
|
|
}
|
|
|
|
void CreateVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) {
|
|
// doesn't care about exception when process was canceled by user
|
|
if (canceled) {
|
|
eptr = nullptr;
|
|
return;
|
|
}
|
|
if (priv::process(eptr)) return;
|
|
if (m_result.its.empty())
|
|
return priv::create_message(_u8L("Can't create empty volume."));
|
|
|
|
priv::create_volume(std::move(m_result), m_input.object_id, m_input.volume_type, m_input.trmat, m_input);
|
|
}
|
|
|
|
|
|
/////////////////
|
|
/// Create Object
|
|
CreateObjectJob::CreateObjectJob(DataCreateObject &&input)
|
|
: m_input(std::move(input))
|
|
{
|
|
assert(priv::check(m_input));
|
|
}
|
|
|
|
void CreateObjectJob::process(Ctl &ctl)
|
|
{
|
|
if (!priv::check(m_input))
|
|
throw std::runtime_error("Bad input data for EmbossCreateObjectJob.");
|
|
|
|
auto was_canceled = [&ctl]()->bool { return ctl.was_canceled(); };
|
|
m_result = priv::create_mesh(m_input, was_canceled, ctl);
|
|
if (was_canceled()) return;
|
|
|
|
// Create new object
|
|
// calculate X,Y offset position for lay on platter in place of
|
|
// mouse click
|
|
Vec2d bed_coor = CameraUtils::get_z0_position(
|
|
m_input.camera, m_input.screen_coor);
|
|
|
|
// check point is on build plate:
|
|
Points bed_shape_;
|
|
bed_shape_.reserve(m_input.bed_shape.size());
|
|
for (const Vec2d &p : m_input.bed_shape)
|
|
bed_shape_.emplace_back(p.cast<int>());
|
|
Polygon bed(bed_shape_);
|
|
if (!bed.contains(bed_coor.cast<int>()))
|
|
// mouse pose is out of build plate so create object in center of plate
|
|
bed_coor = bed.centroid().cast<double>();
|
|
|
|
double z = m_input.text_configuration.style.prop.emboss / 2;
|
|
Vec3d offset(bed_coor.x(), bed_coor.y(), z);
|
|
offset -= m_result.center();
|
|
Transform3d::TranslationType tt(offset.x(), offset.y(), offset.z());
|
|
m_transformation = Transform3d(tt);
|
|
}
|
|
|
|
void CreateObjectJob::finalize(bool canceled, std::exception_ptr &eptr)
|
|
{
|
|
// doesn't care about exception when process was canceled by user
|
|
if (canceled) {
|
|
eptr = nullptr;
|
|
return;
|
|
}
|
|
if (priv::process(eptr)) return;
|
|
|
|
// only for sure
|
|
if (m_result.empty())
|
|
return priv::create_message(_u8L("Can't create empty object."));
|
|
|
|
GUI_App &app = wxGetApp();
|
|
Plater *plater = app.plater();
|
|
ObjectList *obj_list = app.obj_list();
|
|
GLCanvas3D *canvas = plater->canvas3D();
|
|
|
|
plater->take_snapshot(_L("Add Emboss text object"));
|
|
|
|
// Create new object and change selection
|
|
bool center = false;
|
|
obj_list->load_mesh_object(std::move(m_result), m_input.volume_name,
|
|
center, &m_input.text_configuration,
|
|
&m_transformation);
|
|
|
|
// When add new object selection is empty.
|
|
// When cursor move and no one object is selected than
|
|
// Manager::reset_all() So Gizmo could be closed before end of creation object
|
|
GLGizmosManager &manager = canvas->get_gizmos_manager();
|
|
if (manager.get_current_type() != GLGizmosManager::Emboss)
|
|
manager.open_gizmo(GLGizmosManager::Emboss);
|
|
|
|
// redraw scene
|
|
canvas->reload_scene(true);
|
|
}
|
|
|
|
/////////////////
|
|
/// Update Volume
|
|
UpdateJob::UpdateJob(DataUpdate&& input)
|
|
: m_input(std::move(input))
|
|
{
|
|
assert(priv::check(m_input, true));
|
|
}
|
|
|
|
void UpdateJob::process(Ctl &ctl)
|
|
{
|
|
if (!priv::check(m_input))
|
|
throw std::runtime_error("Bad input data for EmbossUpdateJob.");
|
|
|
|
auto was_canceled = [&ctl, &cancel = m_input.cancel]()->bool {
|
|
if (cancel->load()) return true;
|
|
return ctl.was_canceled();
|
|
};
|
|
m_result = priv::try_create_mesh(m_input, m_input.font_file, was_canceled);
|
|
if (was_canceled()) return;
|
|
if (m_result.its.empty())
|
|
throw priv::JobException(_u8L("Created text volume is empty. Change text or font.").c_str());
|
|
|
|
// center triangle mesh
|
|
Vec3d shift = m_result.bounding_box().center();
|
|
m_result.translate(-shift.cast<float>());
|
|
}
|
|
|
|
void UpdateJob::finalize(bool canceled, std::exception_ptr &eptr)
|
|
{
|
|
// doesn't care about exception when process was canceled by user
|
|
if (canceled || m_input.cancel->load()) {
|
|
eptr = nullptr;
|
|
return;
|
|
}
|
|
if (priv::process(eptr)) return;
|
|
priv::update_volume(std::move(m_result), m_input);
|
|
}
|
|
|
|
namespace Slic3r::GUI::Emboss {
|
|
|
|
SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional<size_t> text_volume_id)
|
|
{
|
|
SurfaceVolumeData::ModelSources result;
|
|
result.reserve(volumes.size() - 1);
|
|
for (const ModelVolume *v : volumes) {
|
|
if (text_volume_id.has_value() && v->id().id == *text_volume_id) continue;
|
|
// skip modifiers and negative volumes, ...
|
|
if (!v->is_model_part()) continue;
|
|
const TriangleMesh &tm = v->mesh();
|
|
if (tm.empty()) continue;
|
|
if (tm.its.empty()) continue;
|
|
result.push_back({v->get_mesh_shared_ptr(), v->get_matrix()});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume *text_volume)
|
|
{
|
|
if (text_volume == nullptr) return {};
|
|
if (!text_volume->text_configuration.has_value()) return {};
|
|
const ModelVolumePtrs &volumes = text_volume->get_object()->volumes;
|
|
// no other volume in object
|
|
if (volumes.size() <= 1) return {};
|
|
return create_sources(volumes, text_volume->id().id);
|
|
}
|
|
|
|
} // namespace Slic3r::GUI::Emboss
|
|
|
|
/////////////////
|
|
/// Create Surface volume
|
|
CreateSurfaceVolumeJob::CreateSurfaceVolumeJob(CreateSurfaceVolumeData &&input)
|
|
: m_input(std::move(input))
|
|
{
|
|
assert(priv::check(m_input, true));
|
|
}
|
|
|
|
void CreateSurfaceVolumeJob::process(Ctl &ctl) {
|
|
if (!priv::check(m_input))
|
|
throw std::runtime_error("Bad input data for CreateSurfaceVolumeJob.");
|
|
// check cancelation of process
|
|
auto was_canceled = [&ctl]() -> bool { return ctl.was_canceled(); };
|
|
m_result = priv::cut_surface(m_input, m_input, was_canceled);
|
|
}
|
|
|
|
void CreateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) {
|
|
// doesn't care about exception when process was canceled by user
|
|
if (canceled) return;
|
|
if (priv::process(eptr)) return;
|
|
|
|
// TODO: Find better way to Not center volume data when add !!!
|
|
TriangleMesh mesh = m_result; // Part1: copy
|
|
|
|
priv::create_volume(std::move(m_result), m_input.object_id,
|
|
m_input.volume_type, m_input.text_tr, m_input);
|
|
|
|
// Part2: update volume data
|
|
//auto vol = wxGetApp().plater()->model().objects[m_input.object_idx]->volumes.back();
|
|
//UpdateJob::update_volume(vol, std::move(mesh), m_input.text_configuration, m_input.volume_name);
|
|
}
|
|
|
|
/////////////////
|
|
/// Cut Surface
|
|
UpdateSurfaceVolumeJob::UpdateSurfaceVolumeJob(UpdateSurfaceVolumeData &&input)
|
|
: m_input(std::move(input))
|
|
{
|
|
assert(priv::check(m_input, true));
|
|
}
|
|
|
|
void UpdateSurfaceVolumeJob::process(Ctl &ctl)
|
|
{
|
|
if (!priv::check(m_input))
|
|
throw std::runtime_error("Bad input data for UseSurfaceJob.");
|
|
|
|
// check cancelation of process
|
|
auto was_canceled = [&ctl, &cancel = m_input.cancel]()->bool {
|
|
if (cancel->load()) return true;
|
|
return ctl.was_canceled();
|
|
};
|
|
m_result = priv::cut_surface(m_input, m_input, was_canceled);
|
|
}
|
|
|
|
void UpdateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr)
|
|
{
|
|
// doesn't care about exception when process was canceled by user
|
|
if (m_input.cancel->load()) {
|
|
eptr = nullptr;
|
|
return;
|
|
}
|
|
if (canceled) return;
|
|
if (priv::process(eptr)) return;
|
|
priv::update_volume(std::move(m_result), m_input);
|
|
}
|
|
|
|
////////////////////////////
|
|
/// private namespace implementation
|
|
bool priv::check(const DataBase &input, bool check_fontfile, bool use_surface)
|
|
{
|
|
bool res = true;
|
|
if (check_fontfile) {
|
|
assert(input.font_file.has_value());
|
|
res &= input.font_file.has_value();
|
|
}
|
|
assert(!input.text_configuration.fix_3mf_tr.has_value());
|
|
res &= !input.text_configuration.fix_3mf_tr.has_value();
|
|
assert(!input.text_configuration.text.empty());
|
|
res &= !input.text_configuration.text.empty();
|
|
assert(!input.volume_name.empty());
|
|
res &= !input.volume_name.empty();
|
|
assert(input.text_configuration.style.prop.use_surface == use_surface);
|
|
res &= input.text_configuration.style.prop.use_surface == use_surface;
|
|
return res;
|
|
}
|
|
bool priv::check(const DataCreateVolume &input, bool is_main_thread) {
|
|
bool check_fontfile = false;
|
|
bool res = check((DataBase) input, check_fontfile);
|
|
assert(input.volume_type != ModelVolumeType::INVALID);
|
|
res &= input.volume_type != ModelVolumeType::INVALID;
|
|
assert(input.object_id.id >= 0);
|
|
res &= input.object_id.id >= 0;
|
|
return res;
|
|
}
|
|
bool priv::check(const DataCreateObject &input) {
|
|
bool check_fontfile = false;
|
|
bool res = check((DataBase) input, check_fontfile);
|
|
assert(input.screen_coor.x() >= 0.);
|
|
res &= input.screen_coor.x() >= 0.;
|
|
assert(input.screen_coor.y() >= 0.);
|
|
res &= input.screen_coor.y() >= 0.;
|
|
assert(input.bed_shape.size() >= 3); // at least triangle
|
|
res &= input.bed_shape.size() >= 3;
|
|
return res;
|
|
}
|
|
bool priv::check(const DataUpdate &input, bool is_main_thread, bool use_surface){
|
|
bool check_fontfile = true;
|
|
bool res = check((DataBase) input, check_fontfile, use_surface);
|
|
assert(input.volume_id.id >= 0);
|
|
res &= input.volume_id.id >= 0;
|
|
if (is_main_thread)
|
|
assert(get_volume(wxGetApp().model().objects, input.volume_id) != nullptr);
|
|
assert(input.cancel != nullptr);
|
|
res &= input.cancel != nullptr;
|
|
if (is_main_thread)
|
|
assert(!input.cancel->load());
|
|
return res;
|
|
}
|
|
bool priv::check(const CreateSurfaceVolumeData &input, bool is_main_thread)
|
|
{
|
|
bool use_surface = true;
|
|
bool res = check((DataBase)input, is_main_thread, use_surface);
|
|
assert(!input.sources.empty());
|
|
res &= !input.sources.empty();
|
|
return res;
|
|
}
|
|
bool priv::check(const UpdateSurfaceVolumeData &input, bool is_main_thread){
|
|
bool use_surface = true;
|
|
bool res = check((DataUpdate)input, is_main_thread, use_surface);
|
|
assert(!input.sources.empty());
|
|
res &= !input.sources.empty();
|
|
return res;
|
|
}
|
|
|
|
template<typename Fnc>
|
|
TriangleMesh priv::try_create_mesh(const DataBase &input, FontFileWithCache &font, Fnc was_canceled)
|
|
{
|
|
const TextConfiguration &tc = input.text_configuration;
|
|
const char *text = tc.text.c_str();
|
|
const FontProp &prop = tc.style.prop;
|
|
|
|
assert(font.has_value());
|
|
if (!font.has_value()) return {};
|
|
|
|
ExPolygons shapes = text2shapes(font, text, prop, was_canceled);
|
|
if (shapes.empty()) return {};
|
|
if (was_canceled()) return {};
|
|
|
|
const auto &cn = prop.collection_number;
|
|
unsigned int font_index = (cn.has_value()) ? *cn : 0;
|
|
assert(font_index < font.font_file->infos.size());
|
|
int unit_per_em = font.font_file->infos[font_index].unit_per_em;
|
|
float scale = prop.size_in_mm / unit_per_em;
|
|
float depth = prop.emboss / scale;
|
|
auto projectZ = std::make_unique<ProjectZ>(depth);
|
|
ProjectScale project(std::move(projectZ), scale);
|
|
if (was_canceled()) return {};
|
|
return TriangleMesh(polygons2model(shapes, project));
|
|
}
|
|
|
|
template<typename Fnc>
|
|
TriangleMesh priv::create_mesh(DataBase &input, Fnc was_canceled, Job::Ctl& ctl)
|
|
{
|
|
// It is neccessary to create some shape
|
|
// Emboss text window is opened by creation new emboss text object
|
|
TriangleMesh result;
|
|
if (input.font_file.has_value()) {
|
|
result = try_create_mesh(input, input.font_file, was_canceled);
|
|
if (was_canceled()) return {};
|
|
}
|
|
|
|
if (result.its.empty()) {
|
|
result = priv::create_default_mesh();
|
|
if (was_canceled()) return {};
|
|
// only info
|
|
ctl.call_on_main_thread([]() {
|
|
create_message(_u8L("It is used default volume for embossed "
|
|
"text, try to change text or font for fix it."));
|
|
});
|
|
}
|
|
|
|
assert(!result.its.empty());
|
|
return result;
|
|
}
|
|
|
|
TriangleMesh priv::create_default_mesh()
|
|
{
|
|
// When cant load any font use default object loaded from file
|
|
std::string path = Slic3r::resources_dir() + "/data/embossed_text.obj";
|
|
TriangleMesh triangle_mesh;
|
|
if (!load_obj(path.c_str(), &triangle_mesh)) {
|
|
// when can't load mesh use cube
|
|
return TriangleMesh(its_make_cube(36., 4., 2.5));
|
|
}
|
|
return triangle_mesh;
|
|
}
|
|
|
|
void UpdateJob::update_volume(ModelVolume *volume,
|
|
TriangleMesh &&mesh,
|
|
const TextConfiguration &text_configuration,
|
|
const std::string &volume_name)
|
|
{
|
|
// check inputs
|
|
bool is_valid_input =
|
|
volume != nullptr &&
|
|
!mesh.empty() &&
|
|
!volume_name.empty();
|
|
assert(is_valid_input);
|
|
if (!is_valid_input) return;
|
|
|
|
// update volume
|
|
volume->set_mesh(std::move(mesh));
|
|
volume->set_new_unique_id();
|
|
volume->calculate_convex_hull();
|
|
volume->get_object()->invalidate_bounding_box();
|
|
volume->text_configuration = text_configuration;
|
|
|
|
GUI_App &app = wxGetApp(); // may be move to input
|
|
GLCanvas3D *canvas = app.plater()->canvas3D();
|
|
const Selection &selection = canvas->get_selection();
|
|
const GLVolume *gl_volume = selection.get_volume(*selection.get_volume_idxs().begin());
|
|
int object_idx = gl_volume->object_idx();
|
|
|
|
if (volume->name != volume_name) {
|
|
volume->name = volume_name;
|
|
|
|
// update volume name in right panel( volume / object name)
|
|
int volume_idx = gl_volume->volume_idx();
|
|
ObjectList *obj_list = app.obj_list();
|
|
obj_list->update_name_in_list(object_idx, volume_idx);
|
|
}
|
|
|
|
// update printable state on canvas
|
|
if (volume->type() == ModelVolumeType::MODEL_PART)
|
|
canvas->update_instance_printable_state_for_object((size_t) object_idx);
|
|
|
|
// Move object on bed
|
|
if (GLGizmoEmboss::is_text_object(volume)) volume->get_object()->ensure_on_bed();
|
|
|
|
// redraw scene
|
|
bool refresh_immediately = false;
|
|
canvas->reload_scene(refresh_immediately);
|
|
}
|
|
|
|
void priv::update_volume(TriangleMesh &&mesh, const DataUpdate &data)
|
|
{
|
|
// for sure that some object will be created
|
|
if (mesh.its.empty())
|
|
return priv::create_message("Empty mesh can't be created.");
|
|
|
|
Plater *plater = wxGetApp().plater();
|
|
GLCanvas3D *canvas = plater->canvas3D();
|
|
|
|
// Check emboss gizmo is still open
|
|
GLGizmosManager &manager = canvas->get_gizmos_manager();
|
|
if (manager.get_current_type() != GLGizmosManager::Emboss) return;
|
|
|
|
std::string snap_name = GUI::format(_L("Text: %1%"), data.text_configuration.text);
|
|
Plater::TakeSnapshot snapshot(plater, snap_name, UndoRedo::SnapshotType::GizmoAction);
|
|
ModelVolume *volume = get_volume(plater->model().objects, data.volume_id);
|
|
// could appear when user delete edited volume
|
|
if (volume == nullptr)
|
|
return;
|
|
|
|
// apply fix matrix made by store to .3mf
|
|
const auto &tc = volume->text_configuration;
|
|
assert(tc.has_value());
|
|
if (tc.has_value() && tc->fix_3mf_tr.has_value())
|
|
volume->set_transformation(volume->get_matrix() * tc->fix_3mf_tr->inverse());
|
|
|
|
UpdateJob::update_volume(volume, std::move(mesh), data.text_configuration, data.volume_name);
|
|
}
|
|
|
|
void priv::create_volume(
|
|
TriangleMesh &&mesh, const ObjectID& object_id,
|
|
const ModelVolumeType type, const Transform3d trmat, const DataBase &data)
|
|
{
|
|
GUI_App &app = wxGetApp();
|
|
Plater *plater = app.plater();
|
|
ObjectList *obj_list = app.obj_list();
|
|
GLCanvas3D *canvas = plater->canvas3D();
|
|
ModelObjectPtrs &objects = plater->model().objects;
|
|
|
|
ModelObject *obj = nullptr;
|
|
size_t object_idx = 0;
|
|
for (; object_idx < objects.size(); ++object_idx) {
|
|
ModelObject *o = objects[object_idx];
|
|
if (o->id() == object_id) {
|
|
obj = o;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Parent object for text volume was propably removed.
|
|
// Assumption: User know what he does, so text volume is no more needed.
|
|
if (obj == nullptr)
|
|
return priv::create_message(_u8L("Bad object to create volume."));
|
|
|
|
if (mesh.its.empty())
|
|
return priv::create_message(_u8L("Can't create empty volume."));
|
|
|
|
plater->take_snapshot(_L("Add Emboss text Volume"));
|
|
|
|
// NOTE: be carefull add volume also center mesh !!!
|
|
// So first add simple shape(convex hull is also calculated)
|
|
ModelVolume *volume = obj->add_volume(make_cube(1., 1., 1.), type);
|
|
|
|
// TODO: Refactor to create better way to not set cube at begining
|
|
// Revert mesh centering by set mesh after add cube
|
|
volume->set_mesh(std::move(mesh));
|
|
volume->calculate_convex_hull();
|
|
|
|
|
|
// set a default extruder value, since user can't add it manually
|
|
volume->config.set_key_value("extruder", new ConfigOptionInt(0));
|
|
|
|
// do not allow model reload from disk
|
|
volume->source.is_from_builtin_objects = true;
|
|
|
|
volume->name = data.volume_name; // copy
|
|
volume->text_configuration = data.text_configuration; // copy
|
|
volume->set_transformation(trmat);
|
|
|
|
// update volume name in object list
|
|
// updata selection after new volume added
|
|
// change name of volume in right panel
|
|
// select only actual volume
|
|
// when new volume is created change selection to this volume
|
|
auto add_to_selection = [volume](const ModelVolume *vol) { return vol == volume; };
|
|
wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection(object_idx, add_to_selection);
|
|
if (!sel.IsEmpty()) obj_list->select_item(sel.front());
|
|
|
|
// update printable state on canvas
|
|
if (type == ModelVolumeType::MODEL_PART) canvas->update_instance_printable_state_for_object(object_idx);
|
|
|
|
obj_list->selection_changed();
|
|
|
|
// Now is valid text volume selected open emboss gizmo
|
|
GLGizmosManager &manager = canvas->get_gizmos_manager();
|
|
if (manager.get_current_type() != GLGizmosManager::Emboss)
|
|
manager.open_gizmo(GLGizmosManager::Emboss);
|
|
|
|
// redraw scene
|
|
canvas->reload_scene(true);
|
|
}
|
|
|
|
ModelVolume *priv::get_volume(ModelObjectPtrs &objects,
|
|
const ObjectID &volume_id)
|
|
{
|
|
for (ModelObject *obj : objects)
|
|
for (ModelVolume *vol : obj->volumes)
|
|
if (vol->id() == volume_id) return vol;
|
|
return nullptr;
|
|
};
|
|
|
|
OrthoProject priv::create_projection_for_cut(
|
|
Transform3d tr,
|
|
double shape_scale,
|
|
const std::pair<float, float> &z_range)
|
|
{
|
|
double min_z = z_range.first - priv::safe_extension;
|
|
double max_z = z_range.second + priv::safe_extension;
|
|
assert(min_z < max_z);
|
|
// range between min and max value
|
|
double projection_size = max_z - min_z;
|
|
Matrix3d transformation_for_vector = tr.linear();
|
|
// Projection must be negative value.
|
|
// System of text coordinate
|
|
// X .. from left to right
|
|
// Y .. from bottom to top
|
|
// Z .. from text to eye
|
|
Vec3d untransformed_direction(0., 0., projection_size);
|
|
Vec3d project_direction = transformation_for_vector * untransformed_direction;
|
|
|
|
// Projection is in direction from far plane
|
|
tr.translate(Vec3d(0., 0., min_z));
|
|
tr.scale(shape_scale);
|
|
return OrthoProject(tr, project_direction);
|
|
}
|
|
|
|
OrthoProject3d priv::create_emboss_projection(
|
|
bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut)
|
|
{
|
|
// Offset of clossed side to model
|
|
const float surface_offset = 1e-3f; // [in mm]
|
|
float
|
|
front_move = (is_outside) ? emboss : surface_offset,
|
|
back_move = -((is_outside) ? surface_offset : emboss);
|
|
its_transform(cut, tr.pretranslate(Vec3d(0., 0., front_move)));
|
|
Vec3d from_front_to_back(0., 0., back_move - front_move);
|
|
return OrthoProject3d(from_front_to_back);
|
|
}
|
|
|
|
// input can't be const - cache of font
|
|
TriangleMesh priv::cut_surface(DataBase& input1, const SurfaceVolumeData& input2, std::function<bool()> was_canceled)
|
|
{
|
|
const TextConfiguration &tc = input1.text_configuration;
|
|
const char *text = tc.text.c_str();
|
|
const FontProp &fp = tc.style.prop;
|
|
|
|
ExPolygons shapes = text2shapes(input1.font_file, text, fp, was_canceled);
|
|
if (shapes.empty() || shapes.front().contour.empty())
|
|
throw JobException(_u8L("Font doesn't have any shape for given text.").c_str());
|
|
|
|
if (was_canceled()) return {};
|
|
|
|
// Define alignment of text - left, right, center, top bottom, ....
|
|
BoundingBox bb = get_extents(shapes);
|
|
Point projection_center = bb.center();
|
|
for (ExPolygon &shape : shapes) shape.translate(-projection_center);
|
|
bb.translate(-projection_center);
|
|
|
|
const FontFile &ff = *input1.font_file.font_file;
|
|
double shape_scale = get_shape_scale(fp, ff);
|
|
|
|
const SurfaceVolumeData::ModelSources &sources = input2.sources;
|
|
const SurfaceVolumeData::ModelSource *biggest = nullptr;
|
|
|
|
size_t biggest_count = 0;
|
|
// convert index from (s)ources to (i)ndexed (t)riangle (s)ets
|
|
std::vector<size_t> s_to_itss(sources.size(), std::numeric_limits<size_t>::max());
|
|
std::vector<indexed_triangle_set> itss;
|
|
itss.reserve(sources.size());
|
|
for (const SurfaceVolumeData::ModelSource &s : sources) {
|
|
Transform3d mesh_tr_inv = s.tr.inverse();
|
|
Transform3d cut_projection_tr = mesh_tr_inv * input2.text_tr;
|
|
std::pair<float, float> z_range{0., 1.};
|
|
OrthoProject cut_projection = create_projection_for_cut(cut_projection_tr, shape_scale, z_range);
|
|
// copy only part of source model
|
|
indexed_triangle_set its = its_cut_AoI(s.mesh->its, bb, cut_projection);
|
|
if (its.indices.empty()) continue;
|
|
if (biggest_count < its.vertices.size()) {
|
|
biggest_count = its.vertices.size();
|
|
biggest = &s;
|
|
}
|
|
s_to_itss[&s - &sources.front()] = itss.size();
|
|
itss.emplace_back(std::move(its));
|
|
}
|
|
if (itss.empty()) throw JobException(_u8L("There is no volume in projection direction.").c_str());
|
|
|
|
Transform3d tr_inv = biggest->tr.inverse();
|
|
size_t itss_index = s_to_itss[biggest - &sources.front()];
|
|
BoundingBoxf3 mesh_bb = bounding_box(itss[itss_index]);
|
|
for (const SurfaceVolumeData::ModelSource &s : sources) {
|
|
if (&s == biggest) continue;
|
|
size_t itss_index = s_to_itss[&s - &sources.front()];
|
|
if (itss_index == std::numeric_limits<size_t>::max()) continue;
|
|
Transform3d tr = s.tr * tr_inv;
|
|
indexed_triangle_set &its = itss[itss_index];
|
|
its_transform(its, tr);
|
|
BoundingBoxf3 bb = bounding_box(its);
|
|
mesh_bb.merge(bb);
|
|
}
|
|
|
|
// tr_inv = transformation of mesh inverted
|
|
Transform3d cut_projection_tr = tr_inv * input2.text_tr;
|
|
Transform3d emboss_tr = cut_projection_tr.inverse();
|
|
BoundingBoxf3 mesh_bb_tr = mesh_bb.transformed(emboss_tr);
|
|
std::pair<float, float> z_range{mesh_bb_tr.min.z(), mesh_bb_tr.max.z()};
|
|
OrthoProject cut_projection = create_projection_for_cut(cut_projection_tr, shape_scale, z_range);
|
|
float projection_ratio = (-z_range.first + safe_extension) / (z_range.second - z_range.first + 2 * safe_extension);
|
|
|
|
// Use CGAL to cut surface from triangle mesh
|
|
SurfaceCut cut = cut_surface(shapes, itss, cut_projection, projection_ratio);
|
|
if (cut.empty()) throw JobException(_u8L("There is no valid surface for text projection.").c_str());
|
|
if (was_canceled()) return {};
|
|
|
|
// !! Projection needs to transform cut
|
|
OrthoProject3d projection = create_emboss_projection(input2.is_outside, fp.emboss, emboss_tr, cut);
|
|
|
|
indexed_triangle_set new_its = cut2model(cut, projection);
|
|
assert(!new_its.empty());
|
|
|
|
if (was_canceled()) return {};
|
|
return TriangleMesh(std::move(new_its));
|
|
}
|
|
|
|
bool priv::process(std::exception_ptr &eptr) {
|
|
if (!eptr) return false;
|
|
try {
|
|
std::rethrow_exception(eptr);
|
|
} catch (priv::JobException &e) {
|
|
create_message(e.what());
|
|
eptr = nullptr;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#include <wx/msgdlg.h>
|
|
|
|
void priv::create_message(const std::string &message) {
|
|
wxMessageBox(wxString(message), _L("Issue during embossing the text."),
|
|
wxOK | wxICON_WARNING);
|
|
}
|