#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); }