diff --git a/CMakeLists.txt b/CMakeLists.txt index 418a5d9c2..e19d3147b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,14 @@ set(SLIC3R_GTK "2" CACHE STRING "GTK version to use with wxWidgets on Linux") set(IS_CROSS_COMPILE FALSE) +if (SLIC3R_STATIC) + # Prefer config scripts over find modules. This is helpful when building with + # the static dependencies. Many libraries have their own export scripts + # while having a Find module in standard cmake installation. + # (e.g. CURL) + set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) +endif () + if (APPLE) set(CMAKE_FIND_FRAMEWORK LAST) set(CMAKE_FIND_APPBUNDLE LAST) @@ -437,6 +445,14 @@ include_directories(BEFORE SYSTEM ${EIGEN3_INCLUDE_DIR}) # no matter what. find_package(EXPAT REQUIRED) +add_library(libexpat INTERFACE) + +if (TARGET EXPAT::EXPAT ) + target_link_libraries(libexpat INTERFACE EXPAT::EXPAT) +elseif(TARGET expat::expat) + target_link_libraries(libexpat INTERFACE expat::expat) +endif () + find_package(PNG REQUIRED) set(OpenGL_GL_PREFERENCE "LEGACY") diff --git a/deps/wxWidgets/wxWidgets.cmake b/deps/wxWidgets/wxWidgets.cmake index 29374974b..8056a01d3 100644 --- a/deps/wxWidgets/wxWidgets.cmake +++ b/deps/wxWidgets/wxWidgets.cmake @@ -13,8 +13,8 @@ if (UNIX AND NOT APPLE) # wxWidgets will not use char as the underlying type for endif() prusaslicer_add_cmake_project(wxWidgets - URL https://github.com/prusa3d/wxWidgets/archive/2a0b365df947138c513a888d707d46248d78a341.zip - URL_HASH SHA256=9ab05cd5179196fad4ae702c78eaae9418e73a402cfd390f7438e469b13eb735 + URL https://github.com/prusa3d/wxWidgets/archive/34b524f8d5134a40a90d93a16360d533af2676ae.zip + URL_HASH SHA256=e76ca0dd998905c4dbb86f41f264e6e0468504dc2398f7e7e3bba8dc37de2f45 DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} dep_TIFF dep_JPEG dep_NanoSVG CMAKE_ARGS -DwxBUILD_PRECOMP=ON diff --git a/resources/icons/collapse_btn.svg b/resources/icons/collapse_btn.svg new file mode 100644 index 000000000..4ee221a44 --- /dev/null +++ b/resources/icons/collapse_btn.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/cut_.svg b/resources/icons/cut_.svg new file mode 100644 index 000000000..a7f462bb9 --- /dev/null +++ b/resources/icons/cut_.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/icons/cut_connectors.svg b/resources/icons/cut_connectors.svg new file mode 100644 index 000000000..8cd03aa06 --- /dev/null +++ b/resources/icons/cut_connectors.svg @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/resources/icons/expand_btn.svg b/resources/icons/expand_btn.svg new file mode 100644 index 000000000..32d7f9959 --- /dev/null +++ b/resources/icons/expand_btn.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/resources/localization/list.txt b/resources/localization/list.txt index bc8c9f272..dc30cdcc8 100644 --- a/resources/localization/list.txt +++ b/resources/localization/list.txt @@ -32,6 +32,7 @@ src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp src/slic3r/GUI/Gizmos/GLGizmosManager.cpp src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp src/slic3r/GUI/GUI.cpp diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index ecb157218..500028525 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -426,7 +426,9 @@ int CLI::run(int argc, char **argv) o->cut(Z, m_config.opt_float("cut"), &out); } #else - model.objects.front()->cut(0, m_config.opt_float("cut"), ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::FlipLower); +// model.objects.front()->cut(0, m_config.opt_float("cut"), ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::FlipLower); + model.objects.front()->cut(0, Geometry::assemble_transform(m_config.opt_float("cut")* Vec3d::UnitZ()), + ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::PlaceOnCutUpper); #endif model.delete_object(size_t(0)); } diff --git a/src/imgui/imconfig.h b/src/imgui/imconfig.h index f2c3ef083..678c8fe20 100644 --- a/src/imgui/imconfig.h +++ b/src/imgui/imconfig.h @@ -140,6 +140,7 @@ namespace ImGui const wchar_t CancelButton = 0x14; const wchar_t CancelHoverButton = 0x15; // const wchar_t VarLayerHeightMarker = 0x16; + const wchar_t RevertButton = 0x16; const wchar_t RightArrowButton = 0x18; const wchar_t RightArrowHoverButton = 0x19; @@ -168,6 +169,10 @@ namespace ImGui const wchar_t LegendCOG = 0x2615; const wchar_t LegendShells = 0x2616; const wchar_t LegendToolMarker = 0x2617; + const wchar_t WarningMarkerSmall = 0x2618; + const wchar_t ExpandBtn = 0x2619; + const wchar_t CollapseBtn = 0x2620; + const wchar_t InfoMarkerSmall = 0x2621; // void MyFunction(const char* name, const MyMatrix44& v); } diff --git a/src/libslic3r/ExPolygon.cpp b/src/libslic3r/ExPolygon.cpp index 7e22127cd..4de88101c 100644 --- a/src/libslic3r/ExPolygon.cpp +++ b/src/libslic3r/ExPolygon.cpp @@ -19,6 +19,13 @@ void ExPolygon::scale(double factor) hole.scale(factor); } +void ExPolygon::scale(double factor_x, double factor_y) +{ + contour.scale(factor_x, factor_y); + for (Polygon &hole : holes) + hole.scale(factor_x, factor_y); +} + void ExPolygon::translate(const Point &p) { contour.translate(p); diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index def2be85a..4ea5b9596 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -37,6 +37,7 @@ public: void clear() { contour.points.clear(); holes.clear(); } void scale(double factor); + void scale(double factor_x, double factor_y); void translate(double x, double y) { this->translate(Point(coord_t(x), coord_t(y))); } void translate(const Point &vector); void rotate(double angle); diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 5caff6d3a..f2056b8bd 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -77,6 +77,7 @@ const std::string LAYER_CONFIG_RANGES_FILE = "Metadata/Prusa_Slicer_layer_config const std::string SLA_SUPPORT_POINTS_FILE = "Metadata/Slic3r_PE_sla_support_points.txt"; const std::string SLA_DRAIN_HOLES_FILE = "Metadata/Slic3r_PE_sla_drain_holes.txt"; const std::string CUSTOM_GCODE_PER_PRINT_Z_FILE = "Metadata/Prusa_Slicer_custom_gcode_per_print_z.xml"; +const std::string CUT_INFORMATION_FILE = "Metadata/Prusa_Slicer_cut_information.xml"; static constexpr const char* MODEL_TAG = "model"; static constexpr const char* RESOURCES_TAG = "resources"; @@ -408,6 +409,19 @@ namespace Slic3r { VolumeMetadataList volumes; }; + struct CutObjectInfo + { + struct Connector + { + int volume_id; + int type; + float r_tolerance; + float h_tolerance; + }; + CutObjectBase id; + std::vector connectors; + }; + // Map from a 1 based 3MF object ID to a 0 based ModelObject index inside m_model->objects. typedef std::map IdToModelObjectMap; typedef std::map IdToAliasesMap; @@ -416,6 +430,7 @@ namespace Slic3r { typedef std::map IdToGeometryMap; typedef std::map> IdToLayerHeightsProfileMap; typedef std::map IdToLayerConfigRangesMap; + typedef std::map IdToCutObjectInfoMap; typedef std::map> IdToSlaSupportPointsMap; typedef std::map> IdToSlaDrainHolesMap; @@ -443,6 +458,7 @@ namespace Slic3r { IdToGeometryMap m_geometries; CurrentConfig m_curr_config; IdToMetadataMap m_objects_metadata; + IdToCutObjectInfoMap m_cut_object_infos; IdToLayerHeightsProfileMap m_layer_heights_profiles; IdToLayerConfigRangesMap m_layer_config_ranges; IdToSlaSupportPointsMap m_sla_support_points; @@ -474,6 +490,7 @@ namespace Slic3r { bool _load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions); bool _extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + void _extract_cut_information_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions); void _extract_layer_heights_profile_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions); void _extract_sla_support_points_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); @@ -676,6 +693,10 @@ namespace Slic3r { // extract slic3r layer heights profile file _extract_layer_heights_profile_config_from_archive(archive, stat); } + else if (boost::algorithm::iequals(name, CUT_INFORMATION_FILE)) { + // extract slic3r layer config ranges file + _extract_cut_information_from_archive(archive, stat, config_substitutions); + } else if (boost::algorithm::iequals(name, LAYER_CONFIG_RANGES_FILE)) { // extract slic3r layer config ranges file _extract_layer_config_ranges_from_archive(archive, stat, config_substitutions); @@ -818,6 +839,19 @@ namespace Slic3r { if (!_generate_volumes(*model_object, obj_geometry->second, *volumes_ptr, config_substitutions)) return false; + + // Apply cut information for object if any was loaded + // m_cut_object_ids are indexed by a 1 based model object index. + IdToCutObjectInfoMap::iterator cut_object_info = m_cut_object_infos.find(object.second + 1); + if (cut_object_info != m_cut_object_infos.end()) { + model_object->cut_id = cut_object_info->second.id; + + for (auto connector : cut_object_info->second.connectors) { + assert(0 <= connector.volume_id && connector.volume_id <= int(model_object->volumes.size())); + model_object->volumes[connector.volume_id]->cut_info = + ModelVolume::CutInfo(CutConnectorType(connector.type), connector.r_tolerance, connector.h_tolerance, true); + } + } } // If instances contain a single volume, the volume offset should be 0,0,0 @@ -944,6 +978,65 @@ namespace Slic3r { return true; } + void _3MF_Importer::_extract_cut_information_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions) + { + if (stat.m_uncomp_size > 0) { + std::string buffer((size_t)stat.m_uncomp_size, 0); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + add_error("Error while reading cut information data to buffer"); + return; + } + + std::istringstream iss(buffer); // wrap returned xml to istringstream + pt::ptree objects_tree; + pt::read_xml(iss, objects_tree); + + for (const auto& object : objects_tree.get_child("objects")) { + pt::ptree object_tree = object.second; + int obj_idx = object_tree.get(".id", -1); + if (obj_idx <= 0) { + add_error("Found invalid object id"); + continue; + } + + IdToCutObjectInfoMap::iterator object_item = m_cut_object_infos.find(obj_idx); + if (object_item != m_cut_object_infos.end()) { + add_error("Found duplicated cut_object_id"); + continue; + } + + CutObjectBase cut_id; + std::vector connectors; + + for (const auto& obj_cut_info : object_tree) { + if (obj_cut_info.first == "cut_id") { + pt::ptree cut_id_tree = obj_cut_info.second; + cut_id = CutObjectBase(ObjectID( cut_id_tree.get(".id")), + cut_id_tree.get(".check_sum"), + cut_id_tree.get(".connectors_cnt")); + } + if (obj_cut_info.first == "connectors") { + pt::ptree cut_connectors_tree = obj_cut_info.second; + for (const auto& cut_connector : cut_connectors_tree) { + if (cut_connector.first != "connector") + continue; + pt::ptree connector_tree = cut_connector.second; + CutObjectInfo::Connector connector = {connector_tree.get(".volume_id"), + connector_tree.get(".type"), + connector_tree.get(".r_tolerance"), + connector_tree.get(".h_tolerance")}; + connectors.emplace_back(connector); + } + } + } + + CutObjectInfo cut_info {cut_id, connectors}; + m_cut_object_infos.insert({ obj_idx, cut_info }); + } + } + } + void _3MF_Importer::_extract_print_config_from_archive( mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, @@ -2219,6 +2312,7 @@ namespace Slic3r { bool _add_object_to_model_stream(mz_zip_writer_staged_context &context, unsigned int& object_id, ModelObject& object, BuildItemsList& build_items, VolumeToOffsetsMap& volumes_offsets); bool _add_mesh_to_object_stream(mz_zip_writer_staged_context &context, ModelObject& object, VolumeToOffsetsMap& volumes_offsets); bool _add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items); + bool _add_cut_information_file_to_archive(mz_zip_archive& archive, Model& model); bool _add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model); bool _add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model); bool _add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model); @@ -2281,6 +2375,15 @@ namespace Slic3r { return false; } + // Adds file with information for object cut ("Metadata/Slic3r_PE_cut_information.txt"). + // All information for object cut of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. + // The index differes from the index of an object ID of an object instance of a 3MF file! + if (!_add_cut_information_file_to_archive(archive, model)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + // Adds layer height profile file ("Metadata/Slic3r_PE_layer_heights_profile.txt"). // All layer height profiles of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. // The index differes from the index of an object ID of an object instance of a 3MF file! @@ -2781,6 +2884,67 @@ namespace Slic3r { return true; } + bool _3MF_Exporter::_add_cut_information_file_to_archive(mz_zip_archive& archive, Model& model) + { + std::string out = ""; + pt::ptree tree; + + unsigned int object_cnt = 0; + for (const ModelObject* object : model.objects) { + object_cnt++; + pt::ptree& obj_tree = tree.add("objects.object", ""); + + obj_tree.put(".id", object_cnt); + + // Store info for cut_id + pt::ptree& cut_id_tree = obj_tree.add("cut_id", ""); + + // store cut_id atributes + cut_id_tree.put(".id", object->cut_id.id().id); + cut_id_tree.put(".check_sum", object->cut_id.check_sum()); + cut_id_tree.put(".connectors_cnt", object->cut_id.connectors_cnt()); + + int volume_idx = -1; + for (const ModelVolume* volume : object->volumes) { + ++volume_idx; + if (volume->is_cut_connector()) { + pt::ptree& connectors_tree = obj_tree.add("connectors.connector", ""); + connectors_tree.put(".volume_id", volume_idx); + connectors_tree.put(".type", int(volume->cut_info.connector_type)); + connectors_tree.put(".r_tolerance", volume->cut_info.radius_tolerance); + connectors_tree.put(".h_tolerance", volume->cut_info.height_tolerance); + } + } + } + + if (!tree.empty()) { + std::ostringstream oss; + pt::write_xml(oss, tree); + out = oss.str(); + + // Post processing("beautification") of the output string for a better preview + boost::replace_all(out, ">\n \n ", ">\n "); + boost::replace_all(out, ">\n ", ">\n "); + boost::replace_all(out, ">\n ", ">\n "); + boost::replace_all(out, ">", ">\n "); + // OR just + boost::replace_all(out, "><", ">\n<"); + } + + if (!out.empty()) { + if (!mz_zip_writer_add_mem(&archive, CUT_INFORMATION_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add cut information file to archive"); + return false; + } + } + + return true; + } + bool _3MF_Exporter::_add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model) { assert(is_decimal_separator_point()); diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 9f3c142f8..3c4e00454 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -464,12 +464,25 @@ static constexpr const double volume_threshold_inches = 9.0; // 9 = 3*3*3; bool Model::looks_like_imperial_units() const { - if (this->objects.size() == 0) + if (this->objects.empty()) return false; for (ModelObject* obj : this->objects) - if (obj->get_object_stl_stats().volume < volume_threshold_inches) - return true; + if (obj->get_object_stl_stats().volume < volume_threshold_inches) { + if (!obj->is_cut()) + return true; + bool all_cut_parts_look_like_imperial_units = true; + for (ModelObject* obj_other : this->objects) { + if (obj_other == obj) + continue; + if (obj_other->cut_id.is_equal(obj->cut_id) && obj_other->get_object_stl_stats().volume >= volume_threshold_inches) { + all_cut_parts_look_like_imperial_units = false; + break; + } + } + if (all_cut_parts_look_like_imperial_units) + return true; + } return false; } @@ -613,6 +626,7 @@ ModelObject& ModelObject::assign_copy(const ModelObject &rhs) this->layer_height_profile = rhs.layer_height_profile; this->printable = rhs.printable; this->origin_translation = rhs.origin_translation; + this->cut_id.copy(rhs.cut_id); m_bounding_box = rhs.m_bounding_box; m_bounding_box_valid = rhs.m_bounding_box_valid; m_raw_bounding_box = rhs.m_raw_bounding_box; @@ -715,6 +729,7 @@ ModelVolume* ModelObject::add_volume(const ModelVolume &other, ModelVolumeType t ModelVolume* v = new ModelVolume(this, other); if (type != ModelVolumeType::INVALID && v->type() != type) v->set_type(type); + v->cut_info = other.cut_info; this->volumes.push_back(v); // The volume should already be centered at this point of time when copying shared pointers of the triangle mesh and convex hull. // v->center_geometry_after_creation(); @@ -1189,34 +1204,355 @@ size_t ModelObject::parts_count() const return num; } -ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, ModelObjectCutAttributes attributes) +bool ModelObject::has_connectors() const { - if (! attributes.has(ModelObjectCutAttribute::KeepUpper) && ! attributes.has(ModelObjectCutAttribute::KeepLower)) + assert(is_cut()); + for (const ModelVolume* v : this->volumes) + if (v->cut_info.is_connector) + return true; + + return false; +} + +indexed_triangle_set ModelObject::get_connector_mesh(CutConnectorAttributes connector_attributes) +{ + indexed_triangle_set connector_mesh; + + int sectorCount {1}; + switch (CutConnectorShape(connector_attributes.shape)) { + case CutConnectorShape::Triangle: + sectorCount = 3; + break; + case CutConnectorShape::Square: + sectorCount = 4; + break; + case CutConnectorShape::Circle: + sectorCount = 360; + break; + case CutConnectorShape::Hexagon: + sectorCount = 6; + break; + default: + break; + } + + if (connector_attributes.style == CutConnectorStyle::Prizm) + connector_mesh = its_make_cylinder(1.0, 1.0, (2 * PI / sectorCount)); + else if (connector_attributes.type == CutConnectorType::Plug) + connector_mesh = its_make_cone(1.0, 1.0, (2 * PI / sectorCount)); + else + connector_mesh = its_make_frustum_dowel(1.0, 1.0, sectorCount); + + return connector_mesh; +} + +void ModelObject::apply_cut_connectors(const std::string& new_name) +{ + if (cut_connectors.empty()) + return; + + using namespace Geometry; + + size_t connector_id = cut_id.connectors_cnt(); + for (const CutConnector& connector : cut_connectors) { + TriangleMesh mesh = TriangleMesh(get_connector_mesh(connector.attribs)); + // Mesh will be centered when loading. + ModelVolume* new_volume = add_volume(std::move(mesh), ModelVolumeType::NEGATIVE_VOLUME); + + // Transform the new modifier to be aligned inside the instance + new_volume->set_transformation(assemble_transform(connector.pos) * connector.rotation_m * + scale_transform(Vec3f(connector.radius, connector.radius, connector.height).cast())); + + new_volume->cut_info = { connector.attribs.type, connector.radius_tolerance, connector.height_tolerance }; + new_volume->name = new_name + "-" + std::to_string(++connector_id); + } + cut_id.increase_connectors_cnt(cut_connectors.size()); + + // delete all connectors + cut_connectors.clear(); +} + +void ModelObject::invalidate_cut() +{ + this->cut_id.invalidate(); + for (ModelVolume* volume : this->volumes) + volume->invalidate_cut_info(); +} + +void ModelObject::synchronize_model_after_cut() +{ + for (ModelObject* obj : m_model->objects) { + if (obj == this || obj->cut_id.is_equal(this->cut_id)) + continue; + if (obj->is_cut() && obj->cut_id.has_same_id(this->cut_id)) + obj->cut_id.copy(this->cut_id); + } +} + +void ModelObject::apply_cut_attributes(ModelObjectCutAttributes attributes) +{ + // we don't save cut information, if result will not contains all parts of initial object + if (!attributes.has(ModelObjectCutAttribute::KeepUpper) || !attributes.has(ModelObjectCutAttribute::KeepLower)) + return; + + if (cut_id.id().invalid()) + cut_id.init(); + { + int cut_obj_cnt = -1; + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) cut_obj_cnt++; + if (attributes.has(ModelObjectCutAttribute::KeepLower)) cut_obj_cnt++; + if (attributes.has(ModelObjectCutAttribute::CreateDowels)) cut_obj_cnt++; + if (cut_obj_cnt > 0) + cut_id.increase_check_sum(size_t(cut_obj_cnt)); + } +} + +void ModelObject::clone_for_cut(ModelObject** obj) +{ + (*obj) = ModelObject::new_clone(*this); + (*obj)->set_model(nullptr); + (*obj)->sla_support_points.clear(); + (*obj)->sla_drain_holes.clear(); + (*obj)->sla_points_status = sla::PointsStatus::NoPoints; + (*obj)->clear_volumes(); + (*obj)->input_file.clear(); +} + +void ModelVolume::reset_extra_facets() +{ + this->supported_facets.reset(); + this->seam_facets.reset(); + this->mmu_segmentation_facets.reset(); +} + +void ModelVolume::apply_tolerance() +{ + assert(cut_info.is_connector); + if (cut_info.is_processed) + return; + + Vec3d sf = get_scaling_factor(); +/* + // correct Z offset in respect to the new size + Vec3d pos = vol->get_offset(); + pos[Z] += sf[Z] * 0.5 * vol->cut_info.height_tolerance; + vol->set_offset(pos); +*/ + // make a "hole" wider + sf[X] *= 1. + double(cut_info.radius_tolerance); + sf[Y] *= 1. + double(cut_info.radius_tolerance); + + // make a "hole" dipper + sf[Z] *= 1. + double(cut_info.height_tolerance); + + set_scaling_factor(sf); +} + +void ModelObject::process_connector_cut(ModelVolume* volume, ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, + std::vector& dowels, Vec3d& local_dowels_displace) +{ + assert(volume->cut_info.is_connector); + volume->cut_info.set_processed(); + + const auto volume_matrix = volume->get_matrix(); + + // ! Don't apply instance transformation for the conntectors. + // This transformation is already there + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) { + ModelVolume* vol = upper->add_volume(*volume); + vol->set_transformation(volume_matrix); + vol->apply_tolerance(); + } + if (attributes.has(ModelObjectCutAttribute::KeepLower)) { + ModelVolume* vol = lower->add_volume(*volume); + vol->set_transformation(volume_matrix); + + if (volume->cut_info.connector_type == CutConnectorType::Dowel) + vol->apply_tolerance(); + else + // for lower part change type of connector from NEGATIVE_VOLUME to MODEL_PART if this connector is a plug + vol->set_type(ModelVolumeType::MODEL_PART); + } + if (volume->cut_info.connector_type == CutConnectorType::Dowel && + attributes.has(ModelObjectCutAttribute::CreateDowels)) { + ModelObject* dowel{ nullptr }; + // Clone the object to duplicate instances, materials etc. + clone_for_cut(&dowel); + + // add one more solid part same as connector if this connector is a dowel + ModelVolume* vol = dowel->add_volume(*volume); + vol->set_type(ModelVolumeType::MODEL_PART); + + // But discard rotation and Z-offset for this volume + vol->set_rotation(Vec3d::Zero()); + vol->set_offset(Z, 0.0); + + // Compute the displacement (in instance coordinates) to be applied to place the dowels + local_dowels_displace = lower->full_raw_mesh_bounding_box().size().cwiseProduct(Vec3d(1.0, 1.0, 0.0)); + + dowels.push_back(dowel); + } +} + +void ModelObject::process_modifier_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& inverse_cut_matrix, + ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower) +{ + const auto volume_matrix = instance_matrix * volume->get_matrix(); + + // Modifiers are not cut, but we still need to add the instance transformation + // to the modifier volume transformation to preserve their shape properly. + volume->set_transformation(Geometry::Transformation(volume_matrix)); + + // Some logic for the negative volumes/connectors. Add only needed modifiers + auto bb = volume->mesh().transformed_bounding_box(inverse_cut_matrix * volume_matrix); + bool is_crossed_by_cut = bb.min[Z] <= 0 && bb.max[Z] >= 0; + if (attributes.has(ModelObjectCutAttribute::KeepUpper) && (bb.min[Z] >= 0 || is_crossed_by_cut)) + upper->add_volume(*volume); + if (attributes.has(ModelObjectCutAttribute::KeepLower) && (bb.max[Z] <= 0 || is_crossed_by_cut)) + lower->add_volume(*volume); +} + +static void add_cut_volume(TriangleMesh& mesh, ModelObject* object, const ModelVolume* src_volume, const Transform3d& cut_matrix) +{ + if (mesh.empty()) + return; + + mesh.transform(cut_matrix); + ModelVolume* vol = object->add_volume(mesh); + + vol->name = src_volume->name; + // Don't copy the config's ID. + vol->config.assign_config(src_volume->config); + assert(vol->config.id().valid()); + assert(vol->config.id() != src_volume->config.id()); + vol->set_material(src_volume->material_id(), *src_volume->material()); + vol->cut_info = src_volume->cut_info; +} + +void ModelObject::process_solid_part_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix, + ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, Vec3d& local_displace) +{ + const auto volume_matrix = volume->get_matrix(); + + using namespace Geometry; + + const Transformation cut_transformation = Transformation(cut_matrix); + const Transform3d invert_cut_matrix = cut_transformation.get_rotation_matrix().inverse() * assemble_transform(-1 * cut_transformation.get_offset()); + + // Transform the mesh by the combined transformation matrix. + // Flip the triangles in case the composite transformation is left handed. + TriangleMesh mesh(volume->mesh()); + mesh.transform(invert_cut_matrix * instance_matrix * volume_matrix, true); + + volume->reset_mesh(); + // Reset volume transformation except for offset + const Vec3d offset = volume->get_offset(); + volume->set_transformation(Geometry::Transformation()); + volume->set_offset(offset); + + // Perform cut + + TriangleMesh upper_mesh, lower_mesh; + { + indexed_triangle_set upper_its, lower_its; + cut_mesh(mesh.its, 0.0f, &upper_its, &lower_its); + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) + upper_mesh = TriangleMesh(upper_its); + if (attributes.has(ModelObjectCutAttribute::KeepLower)) + lower_mesh = TriangleMesh(lower_its); + } + + // Add required cut parts to the objects + + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) + add_cut_volume(upper_mesh, upper, volume, cut_matrix); + + if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower_mesh.empty()) { + add_cut_volume(lower_mesh, lower, volume, cut_matrix); + + // Compute the displacement (in instance coordinates) to be applied to place the upper parts + // The upper part displacement is set to half of the lower part bounding box + // this is done in hope at least a part of the upper part will always be visible and draggable + local_displace = lower->full_raw_mesh_bounding_box().size().cwiseProduct(Vec3d(-0.5, -0.5, 0.0)); + } +} + +static void invalidate_translations(ModelObject* object, const ModelInstance* src_instance) +{ + if (!object->origin_translation.isApprox(Vec3d::Zero()) && src_instance->get_offset().isApprox(Vec3d::Zero())) { + object->center_around_origin(); + object->translate_instances(-object->origin_translation); + object->origin_translation = Vec3d::Zero(); + } + else { + object->invalidate_bounding_box(); + object->center_around_origin(); + } +} + +static void reset_instance_transformation(ModelObject* object, size_t src_instance_idx, const Transform3d& cut_matrix, + bool place_on_cut = false, bool flip = false, Vec3d local_displace = Vec3d::Zero()) +{ + using namespace Geometry; + + // Reset instance transformation except offset and Z-rotation + + for (size_t i = 0; i < object->instances.size(); ++i) { + auto& obj_instance = object->instances[i]; + const Vec3d offset = obj_instance->get_offset(); + const double rot_z = obj_instance->get_rotation().z(); + + obj_instance->set_transformation(Transformation()); + + const Vec3d displace = local_displace.isApprox(Vec3d::Zero()) ? Vec3d::Zero() : + assemble_transform(Vec3d::Zero(), obj_instance->get_rotation()) * local_displace; + obj_instance->set_offset(offset + displace); + + Vec3d rotation = Vec3d::Zero(); + if (!flip && !place_on_cut) { + if ( i != src_instance_idx) + rotation[Z] = rot_z; + } + else { + Transform3d rotation_matrix = Transform3d::Identity(); + if (flip) + rotation_matrix = rotation_transform(PI * Vec3d::UnitX()); + + if (place_on_cut) + rotation_matrix = rotation_matrix * Transformation(cut_matrix).get_rotation_matrix().inverse(); + + if (i != src_instance_idx) + rotation_matrix = rotation_transform(rot_z * Vec3d::UnitZ()) * rotation_matrix; + + rotation = Transformation(rotation_matrix).get_rotation(); + } + + obj_instance->set_rotation(rotation); + } +} + +ModelObjectPtrs ModelObject::cut(size_t instance, const Transform3d& cut_matrix, ModelObjectCutAttributes attributes) +{ + if (!attributes.has(ModelObjectCutAttribute::KeepUpper) && !attributes.has(ModelObjectCutAttribute::KeepLower)) return {}; BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - start"; + // apply cut attributes for object + apply_cut_attributes(attributes); + // Clone the object to duplicate instances, materials etc. - ModelObject* upper = attributes.has(ModelObjectCutAttribute::KeepUpper) ? ModelObject::new_clone(*this) : nullptr; - ModelObject* lower = attributes.has(ModelObjectCutAttribute::KeepLower) ? ModelObject::new_clone(*this) : nullptr; + ModelObject* upper{ nullptr }; + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) + clone_for_cut(&upper); - if (attributes.has(ModelObjectCutAttribute::KeepUpper)) { - upper->set_model(nullptr); - upper->sla_support_points.clear(); - upper->sla_drain_holes.clear(); - upper->sla_points_status = sla::PointsStatus::NoPoints; - upper->clear_volumes(); - upper->input_file.clear(); - } + ModelObject* lower{ nullptr }; + if (attributes.has(ModelObjectCutAttribute::KeepLower)) + clone_for_cut(&lower); - if (attributes.has(ModelObjectCutAttribute::KeepLower)) { - lower->set_model(nullptr); - lower->sla_support_points.clear(); - lower->sla_drain_holes.clear(); - lower->sla_points_status = sla::PointsStatus::NoPoints; - lower->clear_volumes(); - lower->input_file.clear(); - } + std::vector dowels; + + using namespace Geometry; // Because transformations are going to be applied to meshes directly, // we reset transformation of all instances and volumes, @@ -1224,128 +1560,72 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, ModelObjectCutAttr // in the transformation matrix and not applied to the mesh transform. // const auto instance_matrix = instances[instance]->get_matrix(true); - const auto instance_matrix = Geometry::assemble_transform( + const auto instance_matrix = assemble_transform( Vec3d::Zero(), // don't apply offset - instances[instance]->get_rotation().cwiseProduct(Vec3d(1.0, 1.0, 0.0)), // don't apply Z-rotation + instances[instance]->get_rotation(), instances[instance]->get_scaling_factor(), instances[instance]->get_mirror() ); - z -= instances[instance]->get_offset().z(); + const Transformation cut_transformation = Transformation(cut_matrix); + const Transform3d inverse_cut_matrix = cut_transformation.get_rotation_matrix().inverse() * assemble_transform(-1. * cut_transformation.get_offset()); // Displacement (in instance coordinates) to be applied to place the upper parts Vec3d local_displace = Vec3d::Zero(); + Vec3d local_dowels_displace = Vec3d::Zero(); - for (ModelVolume *volume : volumes) { - const auto volume_matrix = volume->get_matrix(); + for (ModelVolume* volume : volumes) { + volume->reset_extra_facets(); - volume->supported_facets.reset(); - volume->seam_facets.reset(); - volume->mmu_segmentation_facets.reset(); - - if (! volume->is_model_part()) { - // Modifiers are not cut, but we still need to add the instance transformation - // to the modifier volume transformation to preserve their shape properly. - - volume->set_transformation(Geometry::Transformation(instance_matrix * volume_matrix)); - - if (attributes.has(ModelObjectCutAttribute::KeepUpper)) - upper->add_volume(*volume); - if (attributes.has(ModelObjectCutAttribute::KeepLower)) - lower->add_volume(*volume); - } - else if (! volume->mesh().empty()) { - // Transform the mesh by the combined transformation matrix. - // Flip the triangles in case the composite transformation is left handed. - TriangleMesh mesh(volume->mesh()); - mesh.transform(instance_matrix * volume_matrix, true); - volume->reset_mesh(); - // Reset volume transformation except for offset - const Vec3d offset = volume->get_offset(); - volume->set_transformation(Geometry::Transformation()); - volume->set_offset(offset); - - // Perform cut - TriangleMesh upper_mesh, lower_mesh; - { - indexed_triangle_set upper_its, lower_its; - cut_mesh(mesh.its, float(z), &upper_its, &lower_its); - if (attributes.has(ModelObjectCutAttribute::KeepUpper)) - upper_mesh = TriangleMesh(upper_its); - if (attributes.has(ModelObjectCutAttribute::KeepLower)) - lower_mesh = TriangleMesh(lower_its); - } - - if (attributes.has(ModelObjectCutAttribute::KeepUpper) && ! upper_mesh.empty()) { - ModelVolume* vol = upper->add_volume(upper_mesh); - vol->name = volume->name; - // Don't copy the config's ID. - vol->config.assign_config(volume->config); - assert(vol->config.id().valid()); - assert(vol->config.id() != volume->config.id()); - vol->set_material(volume->material_id(), *volume->material()); - } - if (attributes.has(ModelObjectCutAttribute::KeepLower) && ! lower_mesh.empty()) { - ModelVolume* vol = lower->add_volume(lower_mesh); - vol->name = volume->name; - // Don't copy the config's ID. - vol->config.assign_config(volume->config); - assert(vol->config.id().valid()); - assert(vol->config.id() != volume->config.id()); - vol->set_material(volume->material_id(), *volume->material()); - - // Compute the displacement (in instance coordinates) to be applied to place the upper parts - // The upper part displacement is set to half of the lower part bounding box - // this is done in hope at least a part of the upper part will always be visible and draggable - local_displace = lower->full_raw_mesh_bounding_box().size().cwiseProduct(Vec3d(-0.5, -0.5, 0.0)); - } + if (!volume->is_model_part()) { + if (volume->cut_info.is_processed) + process_modifier_cut(volume, instance_matrix, inverse_cut_matrix, attributes, upper, lower); + else + process_connector_cut(volume, attributes, upper, lower, dowels, local_dowels_displace); } + else if (!volume->mesh().empty()) + process_solid_part_cut(volume, instance_matrix, cut_matrix, attributes, upper, lower, local_displace); } + // Post-process cut parts + ModelObjectPtrs res; - if (attributes.has(ModelObjectCutAttribute::KeepUpper) && upper->volumes.size() > 0) { - if (!upper->origin_translation.isApprox(Vec3d::Zero()) && instances[instance]->get_offset().isApprox(Vec3d::Zero())) { - upper->center_around_origin(); - upper->translate_instances(-upper->origin_translation); - upper->origin_translation = Vec3d::Zero(); - } - - // Reset instance transformation except offset and Z-rotation - for (size_t i = 0; i < instances.size(); ++i) { - auto &instance = upper->instances[i]; - const Vec3d offset = instance->get_offset(); - const double rot_z = instance->get_rotation().z(); - const Vec3d displace = Geometry::assemble_transform(Vec3d::Zero(), instance->get_rotation()) * local_displace; - - instance->set_transformation(Geometry::Transformation()); - instance->set_offset(offset + displace); - instance->set_rotation(Vec3d(0.0, 0.0, rot_z)); - } + if (attributes.has(ModelObjectCutAttribute::KeepUpper) && !upper->volumes.empty()) { + invalidate_translations(upper, instances[instance]); + reset_instance_transformation(upper, instance, cut_matrix, + attributes.has(ModelObjectCutAttribute::PlaceOnCutUpper), + attributes.has(ModelObjectCutAttribute::FlipUpper), + local_displace); res.push_back(upper); } - if (attributes.has(ModelObjectCutAttribute::KeepLower) && lower->volumes.size() > 0) { - if (!lower->origin_translation.isApprox(Vec3d::Zero()) && instances[instance]->get_offset().isApprox(Vec3d::Zero())) { - lower->center_around_origin(); - lower->translate_instances(-lower->origin_translation); - lower->origin_translation = Vec3d::Zero(); - } - // Reset instance transformation except offset and Z-rotation - for (auto *instance : lower->instances) { - const Vec3d offset = instance->get_offset(); - const double rot_z = instance->get_rotation().z(); - instance->set_transformation(Geometry::Transformation()); - instance->set_offset(offset); - instance->set_rotation(Vec3d(attributes.has(ModelObjectCutAttribute::FlipLower) ? Geometry::deg2rad(180.0) : 0.0, 0.0, rot_z)); - } + if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower->volumes.empty()) { + invalidate_translations(lower, instances[instance]); + reset_instance_transformation(lower, instance, cut_matrix, + attributes.has(ModelObjectCutAttribute::PlaceOnCutLower), + attributes.has(ModelObjectCutAttribute::PlaceOnCutLower) ? true : attributes.has(ModelObjectCutAttribute::FlipLower)); res.push_back(lower); } + if (attributes.has(ModelObjectCutAttribute::CreateDowels) && !dowels.empty()) { + for (auto dowel : dowels) { + invalidate_translations(dowel, instances[instance]); + + reset_instance_transformation(dowel, instance, Transform3d::Identity(), false, false, local_dowels_displace); + + local_dowels_displace += dowel->full_raw_mesh_bounding_box().size().cwiseProduct(Vec3d(-1.5, -1.5, 0.0)); + dowel->name += "-Dowel-" + dowel->volumes[0]->name; + res.push_back(dowel); + } + } + BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - end"; + synchronize_model_after_cut(); + return res; } @@ -2289,6 +2569,14 @@ bool model_has_multi_part_objects(const Model &model) return false; } +bool model_has_connectors(const Model &model) +{ + for (const ModelObject *model_object : model.objects) + if (!model_object->cut_connectors.empty()) + return true; + return false; +} + bool model_has_advanced_features(const Model &model) { auto config_is_advanced = [](const ModelConfig &config) { diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 0e5033475..ee611b006 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -1,1256 +1,1399 @@ -#ifndef slic3r_Model_hpp_ -#define slic3r_Model_hpp_ - -#include "libslic3r.h" -#include "enum_bitmask.hpp" -#include "Geometry.hpp" -#include "ObjectID.hpp" -#include "Point.hpp" -#include "PrintConfig.hpp" -#include "Slicing.hpp" -#include "SLA/SupportPoint.hpp" -#include "SLA/Hollowing.hpp" -#include "TriangleMesh.hpp" -#include "Arrange.hpp" -#include "CustomGCode.hpp" -#include "enum_bitmask.hpp" - -#include -#include -#include -#include -#include - -namespace cereal { - class BinaryInputArchive; - class BinaryOutputArchive; - template void load_optional(BinaryInputArchive &ar, std::shared_ptr &ptr); - template void save_optional(BinaryOutputArchive &ar, const std::shared_ptr &ptr); - template void load_by_value(BinaryInputArchive &ar, T &obj); - template void save_by_value(BinaryOutputArchive &ar, const T &obj); -} - -namespace Slic3r { -enum class ConversionType; - -class BuildVolume; -class Model; -class ModelInstance; -class ModelMaterial; -class ModelObject; -class ModelVolume; -class ModelWipeTower; -class Print; -class SLAPrint; -class TriangleSelector; - -namespace UndoRedo { - class StackImpl; -} - -class ModelConfigObject : public ObjectBase, public ModelConfig -{ -private: - friend class cereal::access; - friend class UndoRedo::StackImpl; - friend class ModelObject; - friend class ModelVolume; - friend class ModelMaterial; - - // Constructors to be only called by derived classes. - // Default constructor to assign a unique ID. - explicit ModelConfigObject() = default; - // Constructor with ignored int parameter to assign an invalid ID, to be replaced - // by an existing ID copied from elsewhere. - explicit ModelConfigObject(int) : ObjectBase(-1) {} - // Copy constructor copies the ID. - explicit ModelConfigObject(const ModelConfigObject &cfg) = default; - // Move constructor copies the ID. - explicit ModelConfigObject(ModelConfigObject &&cfg) = default; - - Timestamp timestamp() const throw() override { return this->ModelConfig::timestamp(); } - bool object_id_and_timestamp_match(const ModelConfigObject &rhs) const throw() { return this->id() == rhs.id() && this->timestamp() == rhs.timestamp(); } - - // called by ModelObject::assign_copy() - ModelConfigObject& operator=(const ModelConfigObject &rhs) = default; - ModelConfigObject& operator=(ModelConfigObject &&rhs) = default; - - template void serialize(Archive &ar) { - ar(cereal::base_class(this)); - } -}; - -namespace Internal { - template - class StaticSerializationWrapper - { - public: - StaticSerializationWrapper(T &wrap) : wrapped(wrap) {} - private: - friend class cereal::access; - friend class UndoRedo::StackImpl; - template void load(Archive &ar) { cereal::load_by_value(ar, wrapped); } - template void save(Archive &ar) const { cereal::save_by_value(ar, wrapped); } - T& wrapped; - }; -} - -typedef std::string t_model_material_id; -typedef std::string t_model_material_attribute; -typedef std::map t_model_material_attributes; - -typedef std::map ModelMaterialMap; -typedef std::vector ModelObjectPtrs; -typedef std::vector ModelVolumePtrs; -typedef std::vector ModelInstancePtrs; - -#define OBJECTBASE_DERIVED_COPY_MOVE_CLONE(TYPE) \ - /* Copy a model, copy the IDs. The Print::apply() will call the TYPE::copy() method */ \ - /* to make a private copy for background processing. */ \ - static TYPE* new_copy(const TYPE &rhs) { auto *ret = new TYPE(rhs); assert(ret->id() == rhs.id()); return ret; } \ - static TYPE* new_copy(TYPE &&rhs) { auto *ret = new TYPE(std::move(rhs)); assert(ret->id() == rhs.id()); return ret; } \ - static TYPE make_copy(const TYPE &rhs) { TYPE ret(rhs); assert(ret.id() == rhs.id()); return ret; } \ - static TYPE make_copy(TYPE &&rhs) { TYPE ret(std::move(rhs)); assert(ret.id() == rhs.id()); return ret; } \ - TYPE& assign_copy(const TYPE &rhs); \ - TYPE& assign_copy(TYPE &&rhs); \ - /* Copy a TYPE, generate new IDs. The front end will use this call. */ \ - static TYPE* new_clone(const TYPE &rhs) { \ - /* Default constructor assigning an invalid ID. */ \ - auto obj = new TYPE(-1); \ - obj->assign_clone(rhs); \ - assert(obj->id().valid() && obj->id() != rhs.id()); \ - return obj; \ - } \ - TYPE make_clone(const TYPE &rhs) { \ - /* Default constructor assigning an invalid ID. */ \ - TYPE obj(-1); \ - obj.assign_clone(rhs); \ - assert(obj.id().valid() && obj.id() != rhs.id()); \ - return obj; \ - } \ - TYPE& assign_clone(const TYPE &rhs) { \ - this->assign_copy(rhs); \ - assert(this->id().valid() && this->id() == rhs.id()); \ - this->assign_new_unique_ids_recursive(); \ - assert(this->id().valid() && this->id() != rhs.id()); \ - return *this; \ - } - -// Material, which may be shared across multiple ModelObjects of a single Model. -class ModelMaterial final : public ObjectBase -{ -public: - // Attributes are defined by the AMF file format, but they don't seem to be used by Slic3r for any purpose. - t_model_material_attributes attributes; - // Dynamic configuration storage for the object specific configuration values, overriding the global configuration. - ModelConfigObject config; - - Model* get_model() const { return m_model; } - void apply(const t_model_material_attributes &attributes) - { this->attributes.insert(attributes.begin(), attributes.end()); } - -private: - // Parent, owning this material. - Model *m_model; - - // To be accessed by the Model. - friend class Model; - // Constructor, which assigns a new unique ID to the material and to its config. - ModelMaterial(Model *model) : m_model(model) { assert(this->id().valid()); } - // Copy constructor copies the IDs of the ModelMaterial and its config, and m_model! - ModelMaterial(const ModelMaterial &rhs) = default; - void set_model(Model *model) { m_model = model; } - void set_new_unique_id() { ObjectBase::set_new_unique_id(); this->config.set_new_unique_id(); } - - // To be accessed by the serialization and Undo/Redo code. - friend class cereal::access; - friend class UndoRedo::StackImpl; - // Create an object for deserialization, don't allocate IDs for ModelMaterial and its config. - ModelMaterial() : ObjectBase(-1), config(-1), m_model(nullptr) { assert(this->id().invalid()); assert(this->config.id().invalid()); } - template void serialize(Archive &ar) { - assert(this->id().invalid()); assert(this->config.id().invalid()); - Internal::StaticSerializationWrapper config_wrapper(config); - ar(attributes, config_wrapper); - // assert(this->id().valid()); assert(this->config.id().valid()); - } - - // Disabled methods. - ModelMaterial(ModelMaterial &&rhs) = delete; - ModelMaterial& operator=(const ModelMaterial &rhs) = delete; - ModelMaterial& operator=(ModelMaterial &&rhs) = delete; -}; - -class LayerHeightProfile final : public ObjectWithTimestamp { -public: - // Assign the content if the timestamp differs, don't assign an ObjectID. - void assign(const LayerHeightProfile &rhs) { if (! this->timestamp_matches(rhs)) { m_data = rhs.m_data; this->copy_timestamp(rhs); } } - void assign(LayerHeightProfile &&rhs) { if (! this->timestamp_matches(rhs)) { m_data = std::move(rhs.m_data); this->copy_timestamp(rhs); } } - - std::vector get() const throw() { return m_data; } - bool empty() const throw() { return m_data.empty(); } - void set(const std::vector &data) { if (m_data != data) { m_data = data; this->touch(); } } - void set(std::vector &&data) { if (m_data != data) { m_data = std::move(data); this->touch(); } } - void clear() { m_data.clear(); this->touch(); } - - template void serialize(Archive &ar) - { - ar(cereal::base_class(this), m_data); - } - -private: - // Constructors to be only called by derived classes. - // Default constructor to assign a unique ID. - explicit LayerHeightProfile() = default; - // Constructor with ignored int parameter to assign an invalid ID, to be replaced - // by an existing ID copied from elsewhere. - explicit LayerHeightProfile(int) : ObjectWithTimestamp(-1) {} - // Copy constructor copies the ID. - explicit LayerHeightProfile(const LayerHeightProfile &rhs) = default; - // Move constructor copies the ID. - explicit LayerHeightProfile(LayerHeightProfile &&rhs) = default; - - // called by ModelObject::assign_copy() - LayerHeightProfile& operator=(const LayerHeightProfile &rhs) = default; - LayerHeightProfile& operator=(LayerHeightProfile &&rhs) = default; - - std::vector m_data; - - // to access set_new_unique_id() when copy / pasting an object - friend class ModelObject; -}; - -// Declared outside of ModelVolume, so it could be forward declared. -enum class ModelVolumeType : int { - INVALID = -1, - MODEL_PART = 0, - NEGATIVE_VOLUME, - PARAMETER_MODIFIER, - SUPPORT_BLOCKER, - SUPPORT_ENFORCER, -}; - -enum class ModelObjectCutAttribute : int { KeepUpper, KeepLower, FlipLower }; -using ModelObjectCutAttributes = enum_bitmask; -ENABLE_ENUM_BITMASK_OPERATORS(ModelObjectCutAttribute); - -// A printable object, possibly having multiple print volumes (each with its own set of parameters and materials), -// and possibly having multiple modifier volumes, each modifier volume with its set of parameters and materials. -// Each ModelObject may be instantiated mutliple times, each instance having different placement on the print bed, -// different rotation and different uniform scaling. -class ModelObject final : public ObjectBase -{ -public: - std::string name; - std::string input_file; // XXX: consider fs::path - // Instances of this ModelObject. Each instance defines a shift on the print bed, rotation around the Z axis and a uniform scaling. - // Instances are owned by this ModelObject. - ModelInstancePtrs instances; - // Printable and modifier volumes, each with its material ID and a set of override parameters. - // ModelVolumes are owned by this ModelObject. - ModelVolumePtrs volumes; - // Configuration parameters specific to a single ModelObject, overriding the global Slic3r settings. - ModelConfigObject config; - // Variation of a layer thickness for spans of Z coordinates + optional parameter overrides. - t_layer_config_ranges layer_config_ranges; - // Profile of increasing z to a layer height, to be linearly interpolated when calculating the layers. - // The pairs of are packed into a 1D array. - LayerHeightProfile layer_height_profile; - // Whether or not this object is printable - bool printable; - - // This vector holds position of selected support points for SLA. The data are - // saved in mesh coordinates to allow using them for several instances. - // The format is (x, y, z, point_size, supports_island) - sla::SupportPoints sla_support_points; - // To keep track of where the points came from (used for synchronization between - // the SLA gizmo and the backend). - sla::PointsStatus sla_points_status = sla::PointsStatus::NoPoints; - - // Holes to be drilled into the object so resin can flow out - sla::DrainHoles sla_drain_holes; - - /* This vector accumulates the total translation applied to the object by the - center_around_origin() method. Callers might want to apply the same translation - to new volumes before adding them to this object in order to preserve alignment - when user expects that. */ - Vec3d origin_translation; - - Model* get_model() { return m_model; } - const Model* get_model() const { return m_model; } - - ModelVolume* add_volume(const TriangleMesh &mesh); - ModelVolume* add_volume(TriangleMesh &&mesh, ModelVolumeType type = ModelVolumeType::MODEL_PART); - ModelVolume* add_volume(const ModelVolume &volume, ModelVolumeType type = ModelVolumeType::INVALID); - ModelVolume* add_volume(const ModelVolume &volume, TriangleMesh &&mesh); - void delete_volume(size_t idx); - void clear_volumes(); - void sort_volumes(bool full_sort); - bool is_multiparts() const { return volumes.size() > 1; } - // Checks if any of object volume is painted using the fdm support painting gizmo. - bool is_fdm_support_painted() const; - // Checks if any of object volume is painted using the seam painting gizmo. - bool is_seam_painted() const; - // Checks if any of object volume is painted using the multi-material painting gizmo. - bool is_mm_painted() const; - - ModelInstance* add_instance(); - ModelInstance* add_instance(const ModelInstance &instance); - ModelInstance* add_instance(const Vec3d &offset, const Vec3d &scaling_factor, const Vec3d &rotation, const Vec3d &mirror); - void delete_instance(size_t idx); - void delete_last_instance(); - void clear_instances(); - - // Returns the bounding box of the transformed instances. - // This bounding box is approximate and not snug. - // This bounding box is being cached. - const BoundingBoxf3& bounding_box() const; - void invalidate_bounding_box() { m_bounding_box_valid = false; m_raw_bounding_box_valid = false; m_raw_mesh_bounding_box_valid = false; } - - // A mesh containing all transformed instances of this object. - TriangleMesh mesh() const; - // Non-transformed (non-rotated, non-scaled, non-translated) sum of non-modifier object volumes. - // Currently used by ModelObject::mesh() and to calculate the 2D envelope for 2D plater. - TriangleMesh raw_mesh() const; - // The same as above, but producing a lightweight indexed_triangle_set. - indexed_triangle_set raw_indexed_triangle_set() const; - // A transformed snug bounding box around the non-modifier object volumes, without the translation applied. - // This bounding box is only used for the actual slicing. - const BoundingBoxf3& raw_bounding_box() const; - // A snug bounding box around the transformed non-modifier object volumes. - BoundingBoxf3 instance_bounding_box(size_t instance_idx, bool dont_translate = false) const; - // A snug bounding box of non-transformed (non-rotated, non-scaled, non-translated) sum of non-modifier object volumes. - const BoundingBoxf3& raw_mesh_bounding_box() const; - // A snug bounding box of non-transformed (non-rotated, non-scaled, non-translated) sum of all object volumes. - BoundingBoxf3 full_raw_mesh_bounding_box() const; - - // Calculate 2D convex hull of of a projection of the transformed printable volumes into the XY plane. - // This method is cheap in that it does not make any unnecessary copy of the volume meshes. - // This method is used by the auto arrange function. - Polygon convex_hull_2d(const Transform3d &trafo_instance) const; - - void center_around_origin(bool include_modifiers = true); - void ensure_on_bed(bool allow_negative_z = false); - - void translate_instances(const Vec3d& vector); - void translate_instance(size_t instance_idx, const Vec3d& vector); - void translate(const Vec3d &vector) { this->translate(vector(0), vector(1), vector(2)); } - void translate(double x, double y, double z); - void scale(const Vec3d &versor); - void scale(const double s) { this->scale(Vec3d(s, s, s)); } - void scale(double x, double y, double z) { this->scale(Vec3d(x, y, z)); } - /// Scale the current ModelObject to fit by altering the scaling factor of ModelInstances. - /// It operates on the total size by duplicating the object according to all the instances. - /// \param size Sizef3 the size vector - void scale_to_fit(const Vec3d &size); - void rotate(double angle, Axis axis); - void rotate(double angle, const Vec3d& axis); - void mirror(Axis axis); - - // This method could only be called before the meshes of this ModelVolumes are not shared! - void scale_mesh_after_creation(const float scale); - void convert_units(ModelObjectPtrs&new_objects, ConversionType conv_type, std::vector volume_idxs); - - size_t materials_count() const; - size_t facets_count() const; - size_t parts_count() const; - ModelObjectPtrs cut(size_t instance, coordf_t z, ModelObjectCutAttributes attributes); - void split(ModelObjectPtrs* new_objects); - void merge(); - // Support for non-uniform scaling of instances. If an instance is rotated by angles, which are not multiples of ninety degrees, - // then the scaling in world coordinate system is not representable by the Geometry::Transformation structure. - // This situation is solved by baking in the instance transformation into the mesh vertices. - // Rotation and mirroring is being baked in. In case the instance scaling was non-uniform, it is baked in as well. - void bake_xy_rotation_into_meshes(size_t instance_idx); - - double get_min_z() const; - double get_max_z() const; - double get_instance_min_z(size_t instance_idx) const; - double get_instance_max_z(size_t instance_idx) const; - - // Print object statistics to console. - void print_info() const; - - std::string get_export_filename() const; - - // Get full stl statistics for all object's meshes - TriangleMeshStats get_object_stl_stats() const; - // Get count of errors in the mesh( or all object's meshes, if volume index isn't defined) - int get_repaired_errors_count(const int vol_idx = -1) const; - -private: - friend class Model; - // This constructor assigns new ID to this ModelObject and its config. - explicit ModelObject(Model* model) : m_model(model), printable(true), origin_translation(Vec3d::Zero()), - m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) - { - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->layer_height_profile.id().valid()); - } - explicit ModelObject(int) : ObjectBase(-1), config(-1), layer_height_profile(-1), m_model(nullptr), printable(true), origin_translation(Vec3d::Zero()), m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) - { - assert(this->id().invalid()); - assert(this->config.id().invalid()); - assert(this->layer_height_profile.id().invalid()); - } - ~ModelObject(); - void assign_new_unique_ids_recursive() override; - - // To be able to return an object from own copy / clone methods. Hopefully the compiler will do the "Copy elision" - // (Omits copy and move(since C++11) constructors, resulting in zero - copy pass - by - value semantics). - ModelObject(const ModelObject &rhs) : ObjectBase(-1), config(-1), layer_height_profile(-1), m_model(rhs.m_model) { - assert(this->id().invalid()); - assert(this->config.id().invalid()); - assert(this->layer_height_profile.id().invalid()); - assert(rhs.id() != rhs.config.id()); - assert(rhs.id() != rhs.layer_height_profile.id()); - this->assign_copy(rhs); - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->layer_height_profile.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->layer_height_profile.id()); - assert(this->id() == rhs.id()); - assert(this->config.id() == rhs.config.id()); - assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); - } - explicit ModelObject(ModelObject &&rhs) : ObjectBase(-1), config(-1), layer_height_profile(-1) { - assert(this->id().invalid()); - assert(this->config.id().invalid()); - assert(this->layer_height_profile.id().invalid()); - assert(rhs.id() != rhs.config.id()); - assert(rhs.id() != rhs.layer_height_profile.id()); - this->assign_copy(std::move(rhs)); - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->layer_height_profile.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->layer_height_profile.id()); - assert(this->id() == rhs.id()); - assert(this->config.id() == rhs.config.id()); - assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); - } - ModelObject& operator=(const ModelObject &rhs) { - this->assign_copy(rhs); - m_model = rhs.m_model; - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->layer_height_profile.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->layer_height_profile.id()); - assert(this->id() == rhs.id()); - assert(this->config.id() == rhs.config.id()); - assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); - return *this; - } - ModelObject& operator=(ModelObject &&rhs) { - this->assign_copy(std::move(rhs)); - m_model = rhs.m_model; - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->layer_height_profile.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->layer_height_profile.id()); - assert(this->id() == rhs.id()); - assert(this->config.id() == rhs.config.id()); - assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); - return *this; - } - void set_new_unique_id() { - ObjectBase::set_new_unique_id(); - this->config.set_new_unique_id(); - this->layer_height_profile.set_new_unique_id(); - } - - OBJECTBASE_DERIVED_COPY_MOVE_CLONE(ModelObject) - - // Parent object, owning this ModelObject. Set to nullptr here, so the macros above will have it initialized. - Model *m_model = nullptr; - - // Bounding box, cached. - mutable BoundingBoxf3 m_bounding_box; - mutable bool m_bounding_box_valid; - mutable BoundingBoxf3 m_raw_bounding_box; - mutable bool m_raw_bounding_box_valid; - mutable BoundingBoxf3 m_raw_mesh_bounding_box; - mutable bool m_raw_mesh_bounding_box_valid; - - // Called by Print::apply() to set the model pointer after making a copy. - friend class Print; - friend class SLAPrint; - void set_model(Model *model) { m_model = model; } - - // Undo / Redo through the cereal serialization library - friend class cereal::access; - friend class UndoRedo::StackImpl; - // Used for deserialization -> Don't allocate any IDs for the ModelObject or its config. - ModelObject() : - ObjectBase(-1), config(-1), layer_height_profile(-1), - m_model(nullptr), m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) { - assert(this->id().invalid()); - assert(this->config.id().invalid()); - assert(this->layer_height_profile.id().invalid()); - } - template void serialize(Archive &ar) { - ar(cereal::base_class(this)); - Internal::StaticSerializationWrapper config_wrapper(config); - Internal::StaticSerializationWrapper layer_heigth_profile_wrapper(layer_height_profile); - ar(name, input_file, instances, volumes, config_wrapper, layer_config_ranges, layer_heigth_profile_wrapper, - sla_support_points, sla_points_status, sla_drain_holes, printable, origin_translation, - m_bounding_box, m_bounding_box_valid, m_raw_bounding_box, m_raw_bounding_box_valid, m_raw_mesh_bounding_box, m_raw_mesh_bounding_box_valid); - } - - // Called by Print::validate() from the UI thread. - unsigned int update_instances_print_volume_state(const BuildVolume &build_volume); -}; - -enum class EnforcerBlockerType : int8_t { - // Maximum is 3. The value is serialized in TriangleSelector into 2 bits. - NONE = 0, - ENFORCER = 1, - BLOCKER = 2, - // Maximum is 15. The value is serialized in TriangleSelector into 6 bits using a 2 bit prefix code. - Extruder1 = ENFORCER, - Extruder2 = BLOCKER, - Extruder3, - Extruder4, - Extruder5, - Extruder6, - Extruder7, - Extruder8, - Extruder9, - Extruder10, - Extruder11, - Extruder12, - Extruder13, - Extruder14, - Extruder15, -}; - -enum class ConversionType : int { - CONV_TO_INCH, - CONV_FROM_INCH, - CONV_TO_METER, - CONV_FROM_METER, -}; - -class FacetsAnnotation final : public ObjectWithTimestamp { -public: - // Assign the content if the timestamp differs, don't assign an ObjectID. - void assign(const FacetsAnnotation& rhs) { if (! this->timestamp_matches(rhs)) { m_data = rhs.m_data; this->copy_timestamp(rhs); } } - void assign(FacetsAnnotation&& rhs) { if (! this->timestamp_matches(rhs)) { m_data = std::move(rhs.m_data); this->copy_timestamp(rhs); } } - const std::pair>, std::vector>& get_data() const throw() { return m_data; } - bool set(const TriangleSelector& selector); - indexed_triangle_set get_facets(const ModelVolume& mv, EnforcerBlockerType type) const; - indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const; - bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const; - bool empty() const { return m_data.first.empty(); } - - // Following method clears the config and increases its timestamp, so the deleted - // state is considered changed from perspective of the undo/redo stack. - void reset(); - - // Serialize triangle into string, for serialization into 3MF/AMF. - std::string get_triangle_as_string(int i) const; - - // Before deserialization, reserve space for n_triangles. - void reserve(int n_triangles) { m_data.first.reserve(n_triangles); } - // Deserialize triangles one by one, with strictly increasing triangle_id. - void set_triangle_from_string(int triangle_id, const std::string& str); - // After deserializing the last triangle, shrink data to fit. - void shrink_to_fit() { m_data.first.shrink_to_fit(); m_data.second.shrink_to_fit(); } - -private: - // Constructors to be only called by derived classes. - // Default constructor to assign a unique ID. - explicit FacetsAnnotation() = default; - // Constructor with ignored int parameter to assign an invalid ID, to be replaced - // by an existing ID copied from elsewhere. - explicit FacetsAnnotation(int) : ObjectWithTimestamp(-1) {} - // Copy constructor copies the ID. - explicit FacetsAnnotation(const FacetsAnnotation &rhs) = default; - // Move constructor copies the ID. - explicit FacetsAnnotation(FacetsAnnotation &&rhs) = default; - - // called by ModelVolume::assign_copy() - FacetsAnnotation& operator=(const FacetsAnnotation &rhs) = default; - FacetsAnnotation& operator=(FacetsAnnotation &&rhs) = default; - - friend class cereal::access; - friend class UndoRedo::StackImpl; - - template void serialize(Archive &ar) - { - ar(cereal::base_class(this), m_data); - } - - std::pair>, std::vector> m_data; - - // To access set_new_unique_id() when copy / pasting a ModelVolume. - friend class ModelVolume; -}; - -// An object STL, or a modifier volume, over which a different set of parameters shall be applied. -// ModelVolume instances are owned by a ModelObject. -class ModelVolume final : public ObjectBase -{ -public: - std::string name; - // struct used by reload from disk command to recover data from disk - struct Source - { - std::string input_file; - int object_idx{ -1 }; - int volume_idx{ -1 }; - Vec3d mesh_offset{ Vec3d::Zero() }; - Geometry::Transformation transform; - bool is_converted_from_inches{ false }; - bool is_converted_from_meters{ false }; - bool is_from_builtin_objects{ false }; - - template void serialize(Archive& ar) { - //FIXME Vojtech: Serialize / deserialize only if the Source is set. - // likely testing input_file or object_idx would be sufficient. - ar(input_file, object_idx, volume_idx, mesh_offset, transform, is_converted_from_inches, is_converted_from_meters, is_from_builtin_objects); - } - }; - Source source; - - // The triangular model. - const TriangleMesh& mesh() const { return *m_mesh.get(); } -#if ENABLE_RAYCAST_PICKING - std::shared_ptr mesh_ptr() const { return m_mesh; } -#endif // ENABLE_RAYCAST_PICKING - void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared(mesh); } - void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared(std::move(mesh)); } - void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared(mesh); } - void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared(std::move(mesh)); } - void set_mesh(std::shared_ptr &mesh) { m_mesh = mesh; } - void set_mesh(std::unique_ptr &&mesh) { m_mesh = std::move(mesh); } - void reset_mesh() { m_mesh = std::make_shared(); } - // Configuration parameters specific to an object model geometry or a modifier volume, - // overriding the global Slic3r settings and the ModelObject settings. - ModelConfigObject config; - - // List of mesh facets to be supported/unsupported. - FacetsAnnotation supported_facets; - - // List of seam enforcers/blockers. - FacetsAnnotation seam_facets; - - // List of mesh facets painted for MMU segmentation. - FacetsAnnotation mmu_segmentation_facets; - - // A parent object owning this modifier volume. - ModelObject* get_object() const { return this->object; } - ModelVolumeType type() const { return m_type; } - void set_type(const ModelVolumeType t) { m_type = t; } - bool is_model_part() const { return m_type == ModelVolumeType::MODEL_PART; } - bool is_negative_volume() const { return m_type == ModelVolumeType::NEGATIVE_VOLUME; } - bool is_modifier() const { return m_type == ModelVolumeType::PARAMETER_MODIFIER; } - bool is_support_enforcer() const { return m_type == ModelVolumeType::SUPPORT_ENFORCER; } - bool is_support_blocker() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER; } - bool is_support_modifier() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER || m_type == ModelVolumeType::SUPPORT_ENFORCER; } - t_model_material_id material_id() const { return m_material_id; } - void set_material_id(t_model_material_id material_id); - ModelMaterial* material() const; - void set_material(t_model_material_id material_id, const ModelMaterial &material); - // Extract the current extruder ID based on this ModelVolume's config and the parent ModelObject's config. - // Extruder ID is only valid for FFF. Returns -1 for SLA or if the extruder ID is not applicable (support volumes). - int extruder_id() const; - - bool is_splittable() const; - - // Split this volume, append the result to the object owning this volume. - // Return the number of volumes created from this one. - // This is useful to assign different materials to different volumes of an object. - size_t split(unsigned int max_extruders); - void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); } - void translate(const Vec3d& displacement); - void scale(const Vec3d& scaling_factors); - void scale(double x, double y, double z) { scale(Vec3d(x, y, z)); } - void scale(double s) { scale(Vec3d(s, s, s)); } - void rotate(double angle, Axis axis); - void rotate(double angle, const Vec3d& axis); - void mirror(Axis axis); - - // This method could only be called before the meshes of this ModelVolumes are not shared! - void scale_geometry_after_creation(const Vec3f &versor); - void scale_geometry_after_creation(const float scale) { this->scale_geometry_after_creation(Vec3f(scale, scale, scale)); } - - // Translates the mesh and the convex hull so that the origin of their vertices is in the center of this volume's bounding box. - // Attention! This method may only be called just after ModelVolume creation! It must not be called once the TriangleMesh of this ModelVolume is shared! - void center_geometry_after_creation(bool update_source_offset = true); - - void calculate_convex_hull(); - const TriangleMesh& get_convex_hull() const; - const std::shared_ptr& get_convex_hull_shared_ptr() const { return m_convex_hull; } - // Get count of errors in the mesh - int get_repaired_errors_count() const; - - // Helpers for loading / storing into AMF / 3MF files. - static ModelVolumeType type_from_string(const std::string &s); - static std::string type_to_string(const ModelVolumeType t); - - const Geometry::Transformation& get_transformation() const { return m_transformation; } - void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; } -#if ENABLE_WORLD_COORDINATE - void set_transformation(const Transform3d& trafo) { m_transformation.set_matrix(trafo); } - - Vec3d get_offset() const { return m_transformation.get_offset(); } -#else - void set_transformation(const Transform3d &trafo) { m_transformation.set_from_transform(trafo); } - - const Vec3d& get_offset() const { return m_transformation.get_offset(); } -#endif // ENABLE_WORLD_COORDINATE - - double get_offset(Axis axis) const { return m_transformation.get_offset(axis); } - - void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); } - void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); } - -#if ENABLE_WORLD_COORDINATE - Vec3d get_rotation() const { return m_transformation.get_rotation(); } -#else - const Vec3d& get_rotation() const { return m_transformation.get_rotation(); } -#endif // ENABLE_WORLD_COORDINATE - double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); } - - void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); } - void set_rotation(Axis axis, double rotation) { m_transformation.set_rotation(axis, rotation); } - - Vec3d get_scaling_factor() const { return m_transformation.get_scaling_factor(); } - double get_scaling_factor(Axis axis) const { return m_transformation.get_scaling_factor(axis); } - - void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); } - void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); } - -#if ENABLE_WORLD_COORDINATE - Vec3d get_mirror() const { return m_transformation.get_mirror(); } -#else - const Vec3d& get_mirror() const { return m_transformation.get_mirror(); } -#endif // ENABLE_WORLD_COORDINATE - double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); } - bool is_left_handed() const { return m_transformation.is_left_handed(); } - - void set_mirror(const Vec3d& mirror) { m_transformation.set_mirror(mirror); } - void set_mirror(Axis axis, double mirror) { m_transformation.set_mirror(axis, mirror); } - void convert_from_imperial_units(); - void convert_from_meters(); - -#if ENABLE_WORLD_COORDINATE - const Transform3d& get_matrix() const { return m_transformation.get_matrix(); } - Transform3d get_matrix_no_offset() const { return m_transformation.get_matrix_no_offset(); } -#else - const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const { return m_transformation.get_matrix(dont_translate, dont_rotate, dont_scale, dont_mirror); } -#endif // ENABLE_WORLD_COORDINATE - - void set_new_unique_id() { - ObjectBase::set_new_unique_id(); - this->config.set_new_unique_id(); - this->supported_facets.set_new_unique_id(); - this->seam_facets.set_new_unique_id(); - this->mmu_segmentation_facets.set_new_unique_id(); - } - - bool is_fdm_support_painted() const { return !this->supported_facets.empty(); } - bool is_seam_painted() const { return !this->seam_facets.empty(); } - bool is_mm_painted() const { return !this->mmu_segmentation_facets.empty(); } - -protected: - friend class Print; - friend class SLAPrint; - friend class Model; - friend class ModelObject; - friend void model_volume_list_update_supports(ModelObject& model_object_dst, const ModelObject& model_object_new); - - // Copies IDs of both the ModelVolume and its config. - explicit ModelVolume(const ModelVolume &rhs) = default; - void set_model_object(ModelObject *model_object) { object = model_object; } - void assign_new_unique_ids_recursive() override; - void transform_this_mesh(const Transform3d& t, bool fix_left_handed); - void transform_this_mesh(const Matrix3d& m, bool fix_left_handed); - -private: - // Parent object owning this ModelVolume. - ModelObject* object; - // The triangular model. - std::shared_ptr m_mesh; - // Is it an object to be printed, or a modifier volume? - ModelVolumeType m_type; - t_model_material_id m_material_id; - // The convex hull of this model's mesh. - std::shared_ptr m_convex_hull; - Geometry::Transformation m_transformation; - - // flag to optimize the checking if the volume is splittable - // -1 -> is unknown value (before first cheking) - // 0 -> is not splittable - // 1 -> is splittable - mutable int m_is_splittable{ -1 }; - - ModelVolume(ModelObject *object, const TriangleMesh &mesh, ModelVolumeType type = ModelVolumeType::MODEL_PART) : m_mesh(new TriangleMesh(mesh)), m_type(type), object(object) - { - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->supported_facets.id().valid()); - assert(this->seam_facets.id().valid()); - assert(this->mmu_segmentation_facets.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->supported_facets.id()); - assert(this->id() != this->seam_facets.id()); - assert(this->id() != this->mmu_segmentation_facets.id()); - if (mesh.facets_count() > 1) - calculate_convex_hull(); - } - ModelVolume(ModelObject *object, TriangleMesh &&mesh, TriangleMesh &&convex_hull, ModelVolumeType type = ModelVolumeType::MODEL_PART) : - m_mesh(new TriangleMesh(std::move(mesh))), m_convex_hull(new TriangleMesh(std::move(convex_hull))), m_type(type), object(object) { - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->supported_facets.id().valid()); - assert(this->seam_facets.id().valid()); - assert(this->mmu_segmentation_facets.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->supported_facets.id()); - assert(this->id() != this->seam_facets.id()); - assert(this->id() != this->mmu_segmentation_facets.id()); - } - - // Copying an existing volume, therefore this volume will get a copy of the ID assigned. - ModelVolume(ModelObject *object, const ModelVolume &other) : - ObjectBase(other), - name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull), - config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation), - supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets) - { - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->supported_facets.id().valid()); - assert(this->seam_facets.id().valid()); - assert(this->mmu_segmentation_facets.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->supported_facets.id()); - assert(this->id() != this->seam_facets.id()); - assert(this->id() != this->mmu_segmentation_facets.id()); - assert(this->id() == other.id()); - assert(this->config.id() == other.config.id()); - assert(this->supported_facets.id() == other.supported_facets.id()); - assert(this->seam_facets.id() == other.seam_facets.id()); - assert(this->mmu_segmentation_facets.id() == other.mmu_segmentation_facets.id()); - this->set_material_id(other.material_id()); - } - // Providing a new mesh, therefore this volume will get a new unique ID assigned. - ModelVolume(ModelObject *object, const ModelVolume &other, TriangleMesh &&mesh) : - name(other.name), source(other.source), config(other.config), object(object), m_mesh(new TriangleMesh(std::move(mesh))), m_type(other.m_type), m_transformation(other.m_transformation) - { - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->supported_facets.id().valid()); - assert(this->seam_facets.id().valid()); - assert(this->mmu_segmentation_facets.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->supported_facets.id()); - assert(this->id() != this->seam_facets.id()); - assert(this->id() != this->mmu_segmentation_facets.id()); - assert(this->id() != other.id()); - assert(this->config.id() == other.config.id()); - this->set_material_id(other.material_id()); - this->config.set_new_unique_id(); - if (m_mesh->facets_count() > 1) - calculate_convex_hull(); - assert(this->config.id().valid()); - assert(this->config.id() != other.config.id()); - assert(this->supported_facets.id() != other.supported_facets.id()); - assert(this->seam_facets.id() != other.seam_facets.id()); - assert(this->mmu_segmentation_facets.id() != other.mmu_segmentation_facets.id()); - assert(this->id() != this->config.id()); - assert(this->supported_facets.empty()); - assert(this->seam_facets.empty()); - assert(this->mmu_segmentation_facets.empty()); - } - - ModelVolume& operator=(ModelVolume &rhs) = delete; - - friend class cereal::access; - friend class UndoRedo::StackImpl; - // Used for deserialization, therefore no IDs are allocated. - ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), object(nullptr) { - assert(this->id().invalid()); - assert(this->config.id().invalid()); - assert(this->supported_facets.id().invalid()); - assert(this->seam_facets.id().invalid()); - assert(this->mmu_segmentation_facets.id().invalid()); - } - template void load(Archive &ar) { - bool has_convex_hull; - ar(name, source, m_mesh, m_type, m_material_id, m_transformation, m_is_splittable, has_convex_hull); - cereal::load_by_value(ar, supported_facets); - cereal::load_by_value(ar, seam_facets); - cereal::load_by_value(ar, mmu_segmentation_facets); - cereal::load_by_value(ar, config); - assert(m_mesh); - if (has_convex_hull) { - cereal::load_optional(ar, m_convex_hull); - if (! m_convex_hull && ! m_mesh->empty()) - // The convex hull was released from the Undo / Redo stack to conserve memory. Recalculate it. - this->calculate_convex_hull(); - } else - m_convex_hull.reset(); - } - template void save(Archive &ar) const { - bool has_convex_hull = m_convex_hull.get() != nullptr; - ar(name, source, m_mesh, m_type, m_material_id, m_transformation, m_is_splittable, has_convex_hull); - cereal::save_by_value(ar, supported_facets); - cereal::save_by_value(ar, seam_facets); - cereal::save_by_value(ar, mmu_segmentation_facets); - cereal::save_by_value(ar, config); - if (has_convex_hull) - cereal::save_optional(ar, m_convex_hull); - } -}; - -inline void model_volumes_sort_by_id(ModelVolumePtrs &model_volumes) -{ - std::sort(model_volumes.begin(), model_volumes.end(), [](const ModelVolume *l, const ModelVolume *r) { return l->id() < r->id(); }); -} - -inline const ModelVolume* model_volume_find_by_id(const ModelVolumePtrs &model_volumes, const ObjectID id) -{ - auto it = lower_bound_by_predicate(model_volumes.begin(), model_volumes.end(), [id](const ModelVolume *mv) { return mv->id() < id; }); - return it != model_volumes.end() && (*it)->id() == id ? *it : nullptr; -} - -enum ModelInstanceEPrintVolumeState : unsigned char -{ - ModelInstancePVS_Inside, - ModelInstancePVS_Partly_Outside, - ModelInstancePVS_Fully_Outside, - ModelInstanceNum_BedStates -}; - -// A single instance of a ModelObject. -// Knows the affine transformation of an object. -class ModelInstance final : public ObjectBase -{ -private: - Geometry::Transformation m_transformation; - -public: - // flag showing the position of this instance with respect to the print volume (set by Print::validate() using ModelObject::check_instances_print_volume_state()) - ModelInstanceEPrintVolumeState print_volume_state; - // Whether or not this instance is printable - bool printable; - - ModelObject* get_object() const { return this->object; } - - const Geometry::Transformation& get_transformation() const { return m_transformation; } - void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; } - -#if ENABLE_WORLD_COORDINATE - Vec3d get_offset() const { return m_transformation.get_offset(); } -#else - const Vec3d& get_offset() const { return m_transformation.get_offset(); } -#endif // ENABLE_WORLD_COORDINATE - double get_offset(Axis axis) const { return m_transformation.get_offset(axis); } - - void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); } - void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); } - -#if ENABLE_WORLD_COORDINATE - Vec3d get_rotation() const { return m_transformation.get_rotation(); } -#else - const Vec3d& get_rotation() const { return m_transformation.get_rotation(); } -#endif // ENABLE_WORLD_COORDINATE - double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); } - - void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); } - void set_rotation(Axis axis, double rotation) { m_transformation.set_rotation(axis, rotation); } - -#if ENABLE_WORLD_COORDINATE - Vec3d get_scaling_factor() const { return m_transformation.get_scaling_factor(); } -#else - const Vec3d& get_scaling_factor() const { return m_transformation.get_scaling_factor(); } -#endif // ENABLE_WORLD_COORDINATE - double get_scaling_factor(Axis axis) const { return m_transformation.get_scaling_factor(axis); } - - void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); } - void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); } - -#if ENABLE_WORLD_COORDINATE - Vec3d get_mirror() const { return m_transformation.get_mirror(); } -#else - const Vec3d& get_mirror() const { return m_transformation.get_mirror(); } -#endif // ENABLE_WORLD_COORDINATE - double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); } - bool is_left_handed() const { return m_transformation.is_left_handed(); } - - void set_mirror(const Vec3d& mirror) { m_transformation.set_mirror(mirror); } - void set_mirror(Axis axis, double mirror) { m_transformation.set_mirror(axis, mirror); } - - // To be called on an external mesh - void transform_mesh(TriangleMesh* mesh, bool dont_translate = false) const; - // Calculate a bounding box of a transformed mesh. To be called on an external mesh. - BoundingBoxf3 transform_mesh_bounding_box(const TriangleMesh& mesh, bool dont_translate = false) const; - // Transform an external bounding box. - BoundingBoxf3 transform_bounding_box(const BoundingBoxf3 &bbox, bool dont_translate = false) const; - // Transform an external vector. - Vec3d transform_vector(const Vec3d& v, bool dont_translate = false) const; - // To be called on an external polygon. It does not translate the polygon, only rotates and scales. - void transform_polygon(Polygon* polygon) const; - -#if ENABLE_WORLD_COORDINATE - const Transform3d& get_matrix() const { return m_transformation.get_matrix(); } - Transform3d get_matrix_no_offset() const { return m_transformation.get_matrix_no_offset(); } -#else - const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const { return m_transformation.get_matrix(dont_translate, dont_rotate, dont_scale, dont_mirror); } -#endif // ENABLE_WORLD_COORDINATE - - bool is_printable() const { return object->printable && printable && (print_volume_state == ModelInstancePVS_Inside); } - - // Getting the input polygon for arrange - arrangement::ArrangePolygon get_arrange_polygon() const; - - // Apply the arrange result on the ModelInstance - void apply_arrange_result(const Vec2d& offs, double rotation) - { - // write the transformation data into the model instance - set_rotation(Z, rotation); - set_offset(X, unscale(offs(X))); - set_offset(Y, unscale(offs(Y))); - this->object->invalidate_bounding_box(); - } - -protected: - friend class Print; - friend class SLAPrint; - friend class Model; - friend class ModelObject; - - explicit ModelInstance(const ModelInstance &rhs) = default; - void set_model_object(ModelObject *model_object) { object = model_object; } - -private: - // Parent object, owning this instance. - ModelObject* object; - - // Constructor, which assigns a new unique ID. - explicit ModelInstance(ModelObject* object) : print_volume_state(ModelInstancePVS_Inside), printable(true), object(object) { assert(this->id().valid()); } - // Constructor, which assigns a new unique ID. - explicit ModelInstance(ModelObject *object, const ModelInstance &other) : - m_transformation(other.m_transformation), print_volume_state(ModelInstancePVS_Inside), printable(other.printable), object(object) { assert(this->id().valid() && this->id() != other.id()); } - - explicit ModelInstance(ModelInstance &&rhs) = delete; - ModelInstance& operator=(const ModelInstance &rhs) = delete; - ModelInstance& operator=(ModelInstance &&rhs) = delete; - - friend class cereal::access; - friend class UndoRedo::StackImpl; - // Used for deserialization, therefore no IDs are allocated. - ModelInstance() : ObjectBase(-1), object(nullptr) { assert(this->id().invalid()); } - template void serialize(Archive &ar) { - ar(m_transformation, print_volume_state, printable); - } -}; - - -class ModelWipeTower final : public ObjectBase -{ -public: - Vec2d position; - double rotation; - -private: - friend class cereal::access; - friend class UndoRedo::StackImpl; - friend class Model; - - // Constructors to be only called by derived classes. - // Default constructor to assign a unique ID. - explicit ModelWipeTower() {} - // Constructor with ignored int parameter to assign an invalid ID, to be replaced - // by an existing ID copied from elsewhere. - explicit ModelWipeTower(int) : ObjectBase(-1) {} - // Copy constructor copies the ID. - explicit ModelWipeTower(const ModelWipeTower &cfg) = default; - - // Disabled methods. - ModelWipeTower(ModelWipeTower &&rhs) = delete; - ModelWipeTower& operator=(const ModelWipeTower &rhs) = delete; - ModelWipeTower& operator=(ModelWipeTower &&rhs) = delete; - - // For serialization / deserialization of ModelWipeTower composed into another class into the Undo / Redo stack as a separate object. - template void serialize(Archive &ar) { ar(position, rotation); } -}; - -// The print bed content. -// Description of a triangular model with multiple materials, multiple instances with various affine transformations -// and with multiple modifier meshes. -// A model groups multiple objects, each object having possibly multiple instances, -// all objects may share mutliple materials. -class Model final : public ObjectBase -{ -public: - // Materials are owned by a model and referenced by objects through t_model_material_id. - // Single material may be shared by multiple models. - ModelMaterialMap materials; - // Objects are owned by a model. Each model may have multiple instances, each instance having its own transformation (shift, scale, rotation). - ModelObjectPtrs objects; - // Wipe tower object. - ModelWipeTower wipe_tower; - - // Extensions for color print - CustomGCode::Info custom_gcode_per_print_z; - - // Default constructor assigns a new ID to the model. - Model() { assert(this->id().valid()); } - ~Model() { this->clear_objects(); this->clear_materials(); } - - /* To be able to return an object from own copy / clone methods. Hopefully the compiler will do the "Copy elision" */ - /* (Omits copy and move(since C++11) constructors, resulting in zero - copy pass - by - value semantics). */ - Model(const Model &rhs) : ObjectBase(-1) { assert(this->id().invalid()); this->assign_copy(rhs); assert(this->id().valid()); assert(this->id() == rhs.id()); } - explicit Model(Model &&rhs) : ObjectBase(-1) { assert(this->id().invalid()); this->assign_copy(std::move(rhs)); assert(this->id().valid()); assert(this->id() == rhs.id()); } - Model& operator=(const Model &rhs) { this->assign_copy(rhs); assert(this->id().valid()); assert(this->id() == rhs.id()); return *this; } - Model& operator=(Model &&rhs) { this->assign_copy(std::move(rhs)); assert(this->id().valid()); assert(this->id() == rhs.id()); return *this; } - - OBJECTBASE_DERIVED_COPY_MOVE_CLONE(Model) - - enum class LoadAttribute : int { - AddDefaultInstances, - CheckVersion - }; - using LoadAttributes = enum_bitmask; - - static Model read_from_file( - const std::string& input_file, - DynamicPrintConfig* config = nullptr, ConfigSubstitutionContext* config_substitutions = nullptr, - LoadAttributes options = LoadAttribute::AddDefaultInstances); - static Model read_from_archive( - const std::string& input_file, - DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, - LoadAttributes options = LoadAttribute::AddDefaultInstances); - - // Add a new ModelObject to this Model, generate a new ID for this ModelObject. - ModelObject* add_object(); - ModelObject* add_object(const char *name, const char *path, const TriangleMesh &mesh); - ModelObject* add_object(const char *name, const char *path, TriangleMesh &&mesh); - ModelObject* add_object(const ModelObject &other); - void delete_object(size_t idx); - bool delete_object(ObjectID id); - bool delete_object(ModelObject* object); - void clear_objects(); - - ModelMaterial* add_material(t_model_material_id material_id); - ModelMaterial* add_material(t_model_material_id material_id, const ModelMaterial &other); - ModelMaterial* get_material(t_model_material_id material_id) { - ModelMaterialMap::iterator i = this->materials.find(material_id); - return (i == this->materials.end()) ? nullptr : i->second; - } - - void delete_material(t_model_material_id material_id); - void clear_materials(); - bool add_default_instances(); - // Returns approximate axis aligned bounding box of this model - BoundingBoxf3 bounding_box() const; - // Set the print_volume_state of PrintObject::instances, - // return total number of printable objects. - unsigned int update_print_volume_state(const BuildVolume &build_volume); - // Returns true if any ModelObject was modified. - bool center_instances_around_point(const Vec2d &point); - void translate(coordf_t x, coordf_t y, coordf_t z) { for (ModelObject *o : this->objects) o->translate(x, y, z); } - TriangleMesh mesh() const; - - // Croaks if the duplicated objects do not fit the print bed. - void duplicate_objects_grid(size_t x, size_t y, coordf_t dist); - - bool looks_like_multipart_object() const; - void convert_multipart_object(unsigned int max_extruders); - bool looks_like_imperial_units() const; - void convert_from_imperial_units(bool only_small_volumes); - bool looks_like_saved_in_meters() const; - void convert_from_meters(bool only_small_volumes); - int removed_objects_with_zero_volume(); - - // Ensures that the min z of the model is not negative - void adjust_min_z(); - - void print_info() const { for (const ModelObject *o : this->objects) o->print_info(); } - - // Propose an output file name & path based on the first printable object's name and source input file's path. - std::string propose_export_file_name_and_path() const; - // Propose an output path, replace extension. The new_extension shall contain the initial dot. - std::string propose_export_file_name_and_path(const std::string &new_extension) const; - - // Checks if any of objects is painted using the fdm support painting gizmo. - bool is_fdm_support_painted() const; - // Checks if any of objects is painted using the seam painting gizmo. - bool is_seam_painted() const; - // Checks if any of objects is painted using the multi-material painting gizmo. - bool is_mm_painted() const; - -private: - explicit Model(int) : ObjectBase(-1) { assert(this->id().invalid()); } - void assign_new_unique_ids_recursive(); - void update_links_bottom_up_recursive(); - - friend class cereal::access; - friend class UndoRedo::StackImpl; - template void serialize(Archive &ar) { - Internal::StaticSerializationWrapper wipe_tower_wrapper(wipe_tower); - ar(materials, objects, wipe_tower_wrapper); - } -}; - -ENABLE_ENUM_BITMASK_OPERATORS(Model::LoadAttribute) - -#undef OBJECTBASE_DERIVED_COPY_MOVE_CLONE -#undef OBJECTBASE_DERIVED_PRIVATE_COPY_MOVE - -// Test whether the two models contain the same number of ModelObjects with the same set of IDs -// ordered in the same order. In that case it is not necessary to kill the background processing. -bool model_object_list_equal(const Model &model_old, const Model &model_new); - -// Test whether the new model is just an extension of the old model (new objects were added -// to the end of the original list. In that case it is not necessary to kill the background processing. -bool model_object_list_extended(const Model &model_old, const Model &model_new); - -// Test whether the new ModelObject contains a different set of volumes (or sorted in a different order) -// than the old ModelObject. -bool model_volume_list_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, const ModelVolumeType type); -bool model_volume_list_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, const std::initializer_list &types); - -// Test whether the now ModelObject has newer custom supports data than the old one. -// The function assumes that volumes list is synchronized. -bool model_custom_supports_data_changed(const ModelObject& mo, const ModelObject& mo_new); - -// Test whether the now ModelObject has newer custom seam data than the old one. -// The function assumes that volumes list is synchronized. -bool model_custom_seam_data_changed(const ModelObject& mo, const ModelObject& mo_new); - -// Test whether the now ModelObject has newer MMU segmentation data than the old one. -// The function assumes that volumes list is synchronized. -extern bool model_mmu_segmentation_data_changed(const ModelObject& mo, const ModelObject& mo_new); - -// If the model has multi-part objects, then it is currently not supported by the SLA mode. -// Either the model cannot be loaded, or a SLA printer has to be activated. -bool model_has_multi_part_objects(const Model &model); -// If the model has advanced features, then it cannot be processed in simple mode. -bool model_has_advanced_features(const Model &model); - -#ifndef NDEBUG -// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique. -void check_model_ids_validity(const Model &model); -void check_model_ids_equal(const Model &model1, const Model &model2); -#endif /* NDEBUG */ - -static const float SINKING_Z_THRESHOLD = -0.001f; -static const double SINKING_MIN_Z_THRESHOLD = 0.05; - -} // namespace Slic3r - -namespace cereal -{ - template struct specialize {}; - template struct specialize {}; -} - -#endif /* slic3r_Model_hpp_ */ +#ifndef slic3r_Model_hpp_ +#define slic3r_Model_hpp_ + +#include "libslic3r.h" +#include "enum_bitmask.hpp" +#include "Geometry.hpp" +#include "ObjectID.hpp" +#include "Point.hpp" +#include "PrintConfig.hpp" +#include "Slicing.hpp" +#include "SLA/SupportPoint.hpp" +#include "SLA/Hollowing.hpp" +#include "TriangleMesh.hpp" +#include "Arrange.hpp" +#include "CustomGCode.hpp" +#include "enum_bitmask.hpp" + +#include +#include +#include +#include +#include + +namespace cereal { + class BinaryInputArchive; + class BinaryOutputArchive; + template void load_optional(BinaryInputArchive &ar, std::shared_ptr &ptr); + template void save_optional(BinaryOutputArchive &ar, const std::shared_ptr &ptr); + template void load_by_value(BinaryInputArchive &ar, T &obj); + template void save_by_value(BinaryOutputArchive &ar, const T &obj); +} + +namespace Slic3r { +enum class ConversionType; + +class BuildVolume; +class Model; +class ModelInstance; +class ModelMaterial; +class ModelObject; +class ModelVolume; +class ModelWipeTower; +class Print; +class SLAPrint; +class TriangleSelector; + +namespace UndoRedo { + class StackImpl; +} + +class ModelConfigObject : public ObjectBase, public ModelConfig +{ +private: + friend class cereal::access; + friend class UndoRedo::StackImpl; + friend class ModelObject; + friend class ModelVolume; + friend class ModelMaterial; + + // Constructors to be only called by derived classes. + // Default constructor to assign a unique ID. + explicit ModelConfigObject() = default; + // Constructor with ignored int parameter to assign an invalid ID, to be replaced + // by an existing ID copied from elsewhere. + explicit ModelConfigObject(int) : ObjectBase(-1) {} + // Copy constructor copies the ID. + explicit ModelConfigObject(const ModelConfigObject &cfg) = default; + // Move constructor copies the ID. + explicit ModelConfigObject(ModelConfigObject &&cfg) = default; + + Timestamp timestamp() const throw() override { return this->ModelConfig::timestamp(); } + bool object_id_and_timestamp_match(const ModelConfigObject &rhs) const throw() { return this->id() == rhs.id() && this->timestamp() == rhs.timestamp(); } + + // called by ModelObject::assign_copy() + ModelConfigObject& operator=(const ModelConfigObject &rhs) = default; + ModelConfigObject& operator=(ModelConfigObject &&rhs) = default; + + template void serialize(Archive &ar) { + ar(cereal::base_class(this)); + } +}; + +namespace Internal { + template + class StaticSerializationWrapper + { + public: + StaticSerializationWrapper(T &wrap) : wrapped(wrap) {} + private: + friend class cereal::access; + friend class UndoRedo::StackImpl; + template void load(Archive &ar) { cereal::load_by_value(ar, wrapped); } + template void save(Archive &ar) const { cereal::save_by_value(ar, wrapped); } + T& wrapped; + }; +} + +typedef std::string t_model_material_id; +typedef std::string t_model_material_attribute; +typedef std::map t_model_material_attributes; + +typedef std::map ModelMaterialMap; +typedef std::vector ModelObjectPtrs; +typedef std::vector ModelVolumePtrs; +typedef std::vector ModelInstancePtrs; + +#define OBJECTBASE_DERIVED_COPY_MOVE_CLONE(TYPE) \ + /* Copy a model, copy the IDs. The Print::apply() will call the TYPE::copy() method */ \ + /* to make a private copy for background processing. */ \ + static TYPE* new_copy(const TYPE &rhs) { auto *ret = new TYPE(rhs); assert(ret->id() == rhs.id()); return ret; } \ + static TYPE* new_copy(TYPE &&rhs) { auto *ret = new TYPE(std::move(rhs)); assert(ret->id() == rhs.id()); return ret; } \ + static TYPE make_copy(const TYPE &rhs) { TYPE ret(rhs); assert(ret.id() == rhs.id()); return ret; } \ + static TYPE make_copy(TYPE &&rhs) { TYPE ret(std::move(rhs)); assert(ret.id() == rhs.id()); return ret; } \ + TYPE& assign_copy(const TYPE &rhs); \ + TYPE& assign_copy(TYPE &&rhs); \ + /* Copy a TYPE, generate new IDs. The front end will use this call. */ \ + static TYPE* new_clone(const TYPE &rhs) { \ + /* Default constructor assigning an invalid ID. */ \ + auto obj = new TYPE(-1); \ + obj->assign_clone(rhs); \ + assert(obj->id().valid() && obj->id() != rhs.id()); \ + return obj; \ + } \ + TYPE make_clone(const TYPE &rhs) { \ + /* Default constructor assigning an invalid ID. */ \ + TYPE obj(-1); \ + obj.assign_clone(rhs); \ + assert(obj.id().valid() && obj.id() != rhs.id()); \ + return obj; \ + } \ + TYPE& assign_clone(const TYPE &rhs) { \ + this->assign_copy(rhs); \ + assert(this->id().valid() && this->id() == rhs.id()); \ + this->assign_new_unique_ids_recursive(); \ + assert(this->id().valid() && this->id() != rhs.id()); \ + return *this; \ + } + +// Material, which may be shared across multiple ModelObjects of a single Model. +class ModelMaterial final : public ObjectBase +{ +public: + // Attributes are defined by the AMF file format, but they don't seem to be used by Slic3r for any purpose. + t_model_material_attributes attributes; + // Dynamic configuration storage for the object specific configuration values, overriding the global configuration. + ModelConfigObject config; + + Model* get_model() const { return m_model; } + void apply(const t_model_material_attributes &attributes) + { this->attributes.insert(attributes.begin(), attributes.end()); } + +private: + // Parent, owning this material. + Model *m_model; + + // To be accessed by the Model. + friend class Model; + // Constructor, which assigns a new unique ID to the material and to its config. + ModelMaterial(Model *model) : m_model(model) { assert(this->id().valid()); } + // Copy constructor copies the IDs of the ModelMaterial and its config, and m_model! + ModelMaterial(const ModelMaterial &rhs) = default; + void set_model(Model *model) { m_model = model; } + void set_new_unique_id() { ObjectBase::set_new_unique_id(); this->config.set_new_unique_id(); } + + // To be accessed by the serialization and Undo/Redo code. + friend class cereal::access; + friend class UndoRedo::StackImpl; + // Create an object for deserialization, don't allocate IDs for ModelMaterial and its config. + ModelMaterial() : ObjectBase(-1), config(-1), m_model(nullptr) { assert(this->id().invalid()); assert(this->config.id().invalid()); } + template void serialize(Archive &ar) { + assert(this->id().invalid()); assert(this->config.id().invalid()); + Internal::StaticSerializationWrapper config_wrapper(config); + ar(attributes, config_wrapper); + // assert(this->id().valid()); assert(this->config.id().valid()); + } + + // Disabled methods. + ModelMaterial(ModelMaterial &&rhs) = delete; + ModelMaterial& operator=(const ModelMaterial &rhs) = delete; + ModelMaterial& operator=(ModelMaterial &&rhs) = delete; +}; + +class LayerHeightProfile final : public ObjectWithTimestamp { +public: + // Assign the content if the timestamp differs, don't assign an ObjectID. + void assign(const LayerHeightProfile &rhs) { if (! this->timestamp_matches(rhs)) { m_data = rhs.m_data; this->copy_timestamp(rhs); } } + void assign(LayerHeightProfile &&rhs) { if (! this->timestamp_matches(rhs)) { m_data = std::move(rhs.m_data); this->copy_timestamp(rhs); } } + + std::vector get() const throw() { return m_data; } + bool empty() const throw() { return m_data.empty(); } + void set(const std::vector &data) { if (m_data != data) { m_data = data; this->touch(); } } + void set(std::vector &&data) { if (m_data != data) { m_data = std::move(data); this->touch(); } } + void clear() { m_data.clear(); this->touch(); } + + template void serialize(Archive &ar) + { + ar(cereal::base_class(this), m_data); + } + +private: + // Constructors to be only called by derived classes. + // Default constructor to assign a unique ID. + explicit LayerHeightProfile() = default; + // Constructor with ignored int parameter to assign an invalid ID, to be replaced + // by an existing ID copied from elsewhere. + explicit LayerHeightProfile(int) : ObjectWithTimestamp(-1) {} + // Copy constructor copies the ID. + explicit LayerHeightProfile(const LayerHeightProfile &rhs) = default; + // Move constructor copies the ID. + explicit LayerHeightProfile(LayerHeightProfile &&rhs) = default; + + // called by ModelObject::assign_copy() + LayerHeightProfile& operator=(const LayerHeightProfile &rhs) = default; + LayerHeightProfile& operator=(LayerHeightProfile &&rhs) = default; + + std::vector m_data; + + // to access set_new_unique_id() when copy / pasting an object + friend class ModelObject; +}; + +enum class CutConnectorType : int { + Plug + , Dowel + , Undef +}; + +enum class CutConnectorStyle : int { + Prizm + , Frustum + , Undef + //,Claw +}; + +enum class CutConnectorShape : int { + Triangle + , Square + , Hexagon + , Circle + , Undef + //,D-shape +}; + +struct CutConnectorAttributes +{ + CutConnectorType type{ CutConnectorType::Plug }; + CutConnectorStyle style{ CutConnectorStyle::Prizm }; + CutConnectorShape shape{ CutConnectorShape::Circle }; + + CutConnectorAttributes() {} + + CutConnectorAttributes(CutConnectorType t, CutConnectorStyle st, CutConnectorShape sh) + : type(t), style(st), shape(sh) + {} + + CutConnectorAttributes(const CutConnectorAttributes& rhs) : + CutConnectorAttributes(rhs.type, rhs.style, rhs.shape) {} + + bool operator==(const CutConnectorAttributes& other) const; + + bool operator!=(const CutConnectorAttributes& other) const { return !(other == (*this)); } + + bool operator<(const CutConnectorAttributes& other) const { + return this->type < other.type || + (this->type == other.type && this->style < other.style) || + (this->type == other.type && this->style == other.style && this->shape < other.shape); + } + + template inline void serialize(Archive& ar) { + ar(type, style, shape); + } +}; + +struct CutConnector +{ + Vec3d pos; + Transform3d rotation_m; + float radius; + float height; + float radius_tolerance;// [0.f : 1.f] + float height_tolerance;// [0.f : 1.f] + CutConnectorAttributes attribs; + + CutConnector() + : pos(Vec3d::Zero()), rotation_m(Transform3d::Identity()), radius(5.f), height(10.f), radius_tolerance(0.f), height_tolerance(0.1f) + {} + + CutConnector(Vec3d p, Transform3d rot, float r, float h, float rt, float ht, CutConnectorAttributes attributes) + : pos(p), rotation_m(rot), radius(r), height(h), radius_tolerance(rt), height_tolerance(ht), attribs(attributes) + {} + + CutConnector(const CutConnector& rhs) : + CutConnector(rhs.pos, rhs.rotation_m, rhs.radius, rhs.height, rhs.radius_tolerance, rhs.height_tolerance, rhs.attribs) {} + + bool operator==(const CutConnector& other) const; + + bool operator!=(const CutConnector& other) const { return !(other == (*this)); } + + template inline void serialize(Archive& ar) { + ar(pos, rotation_m, radius, height, radius_tolerance, height_tolerance, attribs); + } +}; + +using CutConnectors = std::vector; + + +// Declared outside of ModelVolume, so it could be forward declared. +enum class ModelVolumeType : int { + INVALID = -1, + MODEL_PART = 0, + NEGATIVE_VOLUME, + PARAMETER_MODIFIER, + SUPPORT_BLOCKER, + SUPPORT_ENFORCER, +}; + +enum class ModelObjectCutAttribute : int { KeepUpper, KeepLower, FlipUpper, FlipLower, PlaceOnCutUpper, PlaceOnCutLower, CreateDowels }; +using ModelObjectCutAttributes = enum_bitmask; +ENABLE_ENUM_BITMASK_OPERATORS(ModelObjectCutAttribute); + +// A printable object, possibly having multiple print volumes (each with its own set of parameters and materials), +// and possibly having multiple modifier volumes, each modifier volume with its set of parameters and materials. +// Each ModelObject may be instantiated mutliple times, each instance having different placement on the print bed, +// different rotation and different uniform scaling. +class ModelObject final : public ObjectBase +{ +public: + std::string name; + std::string input_file; // XXX: consider fs::path + // Instances of this ModelObject. Each instance defines a shift on the print bed, rotation around the Z axis and a uniform scaling. + // Instances are owned by this ModelObject. + ModelInstancePtrs instances; + // Printable and modifier volumes, each with its material ID and a set of override parameters. + // ModelVolumes are owned by this ModelObject. + ModelVolumePtrs volumes; + // Configuration parameters specific to a single ModelObject, overriding the global Slic3r settings. + ModelConfigObject config; + // Variation of a layer thickness for spans of Z coordinates + optional parameter overrides. + t_layer_config_ranges layer_config_ranges; + // Profile of increasing z to a layer height, to be linearly interpolated when calculating the layers. + // The pairs of are packed into a 1D array. + LayerHeightProfile layer_height_profile; + // Whether or not this object is printable + bool printable; + + // This vector holds position of selected support points for SLA. The data are + // saved in mesh coordinates to allow using them for several instances. + // The format is (x, y, z, point_size, supports_island) + sla::SupportPoints sla_support_points; + // To keep track of where the points came from (used for synchronization between + // the SLA gizmo and the backend). + sla::PointsStatus sla_points_status = sla::PointsStatus::NoPoints; + + // Holes to be drilled into the object so resin can flow out + sla::DrainHoles sla_drain_holes; + + // Connectors to be added into the object before cut and are used to create a solid/negative volumes during a cut perform + CutConnectors cut_connectors; + CutObjectBase cut_id; + + /* This vector accumulates the total translation applied to the object by the + center_around_origin() method. Callers might want to apply the same translation + to new volumes before adding them to this object in order to preserve alignment + when user expects that. */ + Vec3d origin_translation; + + Model* get_model() { return m_model; } + const Model* get_model() const { return m_model; } + + ModelVolume* add_volume(const TriangleMesh &mesh); + ModelVolume* add_volume(TriangleMesh &&mesh, ModelVolumeType type = ModelVolumeType::MODEL_PART); + ModelVolume* add_volume(const ModelVolume &volume, ModelVolumeType type = ModelVolumeType::INVALID); + ModelVolume* add_volume(const ModelVolume &volume, TriangleMesh &&mesh); + void delete_volume(size_t idx); + void clear_volumes(); + void sort_volumes(bool full_sort); + bool is_multiparts() const { return volumes.size() > 1; } + // Checks if any of object volume is painted using the fdm support painting gizmo. + bool is_fdm_support_painted() const; + // Checks if any of object volume is painted using the seam painting gizmo. + bool is_seam_painted() const; + // Checks if any of object volume is painted using the multi-material painting gizmo. + bool is_mm_painted() const; + + ModelInstance* add_instance(); + ModelInstance* add_instance(const ModelInstance &instance); + ModelInstance* add_instance(const Vec3d &offset, const Vec3d &scaling_factor, const Vec3d &rotation, const Vec3d &mirror); + void delete_instance(size_t idx); + void delete_last_instance(); + void clear_instances(); + + // Returns the bounding box of the transformed instances. + // This bounding box is approximate and not snug. + // This bounding box is being cached. + const BoundingBoxf3& bounding_box() const; + void invalidate_bounding_box() { m_bounding_box_valid = false; m_raw_bounding_box_valid = false; m_raw_mesh_bounding_box_valid = false; } + + // A mesh containing all transformed instances of this object. + TriangleMesh mesh() const; + // Non-transformed (non-rotated, non-scaled, non-translated) sum of non-modifier object volumes. + // Currently used by ModelObject::mesh() and to calculate the 2D envelope for 2D plater. + TriangleMesh raw_mesh() const; + // The same as above, but producing a lightweight indexed_triangle_set. + indexed_triangle_set raw_indexed_triangle_set() const; + // A transformed snug bounding box around the non-modifier object volumes, without the translation applied. + // This bounding box is only used for the actual slicing. + const BoundingBoxf3& raw_bounding_box() const; + // A snug bounding box around the transformed non-modifier object volumes. + BoundingBoxf3 instance_bounding_box(size_t instance_idx, bool dont_translate = false) const; + // A snug bounding box of non-transformed (non-rotated, non-scaled, non-translated) sum of non-modifier object volumes. + const BoundingBoxf3& raw_mesh_bounding_box() const; + // A snug bounding box of non-transformed (non-rotated, non-scaled, non-translated) sum of all object volumes. + BoundingBoxf3 full_raw_mesh_bounding_box() const; + + // Calculate 2D convex hull of of a projection of the transformed printable volumes into the XY plane. + // This method is cheap in that it does not make any unnecessary copy of the volume meshes. + // This method is used by the auto arrange function. + Polygon convex_hull_2d(const Transform3d &trafo_instance) const; + + void center_around_origin(bool include_modifiers = true); + void ensure_on_bed(bool allow_negative_z = false); + + void translate_instances(const Vec3d& vector); + void translate_instance(size_t instance_idx, const Vec3d& vector); + void translate(const Vec3d &vector) { this->translate(vector(0), vector(1), vector(2)); } + void translate(double x, double y, double z); + void scale(const Vec3d &versor); + void scale(const double s) { this->scale(Vec3d(s, s, s)); } + void scale(double x, double y, double z) { this->scale(Vec3d(x, y, z)); } + /// Scale the current ModelObject to fit by altering the scaling factor of ModelInstances. + /// It operates on the total size by duplicating the object according to all the instances. + /// \param size Sizef3 the size vector + void scale_to_fit(const Vec3d &size); + void rotate(double angle, Axis axis); + void rotate(double angle, const Vec3d& axis); + void mirror(Axis axis); + + // This method could only be called before the meshes of this ModelVolumes are not shared! + void scale_mesh_after_creation(const float scale); + void convert_units(ModelObjectPtrs&new_objects, ConversionType conv_type, std::vector volume_idxs); + + size_t materials_count() const; + size_t facets_count() const; + size_t parts_count() const; + static indexed_triangle_set get_connector_mesh(CutConnectorAttributes connector_attributes); + void apply_cut_connectors(const std::string& name); + // invalidate cut state for this object and its connectors/volumes + void invalidate_cut(); + void synchronize_model_after_cut(); + void apply_cut_attributes(ModelObjectCutAttributes attributes); + void clone_for_cut(ModelObject **obj); + void process_connector_cut(ModelVolume* volume, ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, + std::vector& dowels, Vec3d& local_dowels_displace); + void process_modifier_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& inverse_cut_matrix, + ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower); + void process_solid_part_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix, + ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, Vec3d& local_displace); + ModelObjectPtrs cut(size_t instance, const Transform3d&cut_matrix, ModelObjectCutAttributes attributes); + void split(ModelObjectPtrs*new_objects); + void merge(); + // Support for non-uniform scaling of instances. If an instance is rotated by angles, which are not multiples of ninety degrees, + // then the scaling in world coordinate system is not representable by the Geometry::Transformation structure. + // This situation is solved by baking in the instance transformation into the mesh vertices. + // Rotation and mirroring is being baked in. In case the instance scaling was non-uniform, it is baked in as well. + void bake_xy_rotation_into_meshes(size_t instance_idx); + + double get_min_z() const; + double get_max_z() const; + double get_instance_min_z(size_t instance_idx) const; + double get_instance_max_z(size_t instance_idx) const; + + // Print object statistics to console. + void print_info() const; + + std::string get_export_filename() const; + + // Get full stl statistics for all object's meshes + TriangleMeshStats get_object_stl_stats() const; + // Get count of errors in the mesh( or all object's meshes, if volume index isn't defined) + int get_repaired_errors_count(const int vol_idx = -1) const; + + bool is_cut() const { return cut_id.id().valid(); } + bool has_connectors() const; + +private: + friend class Model; + // This constructor assigns new ID to this ModelObject and its config. + explicit ModelObject(Model* model) : m_model(model), printable(true), origin_translation(Vec3d::Zero()), + m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) + { + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->layer_height_profile.id().valid()); + } + explicit ModelObject(int) : ObjectBase(-1), config(-1), layer_height_profile(-1), m_model(nullptr), printable(true), origin_translation(Vec3d::Zero()), m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) + { + assert(this->id().invalid()); + assert(this->config.id().invalid()); + assert(this->layer_height_profile.id().invalid()); + } + ~ModelObject(); + void assign_new_unique_ids_recursive() override; + + // To be able to return an object from own copy / clone methods. Hopefully the compiler will do the "Copy elision" + // (Omits copy and move(since C++11) constructors, resulting in zero - copy pass - by - value semantics). + ModelObject(const ModelObject &rhs) : ObjectBase(-1), config(-1), layer_height_profile(-1), m_model(rhs.m_model) { + assert(this->id().invalid()); + assert(this->config.id().invalid()); + assert(this->layer_height_profile.id().invalid()); + assert(rhs.id() != rhs.config.id()); + assert(rhs.id() != rhs.layer_height_profile.id()); + this->assign_copy(rhs); + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->layer_height_profile.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->layer_height_profile.id()); + assert(this->id() == rhs.id()); + assert(this->config.id() == rhs.config.id()); + assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); + } + explicit ModelObject(ModelObject &&rhs) : ObjectBase(-1), config(-1), layer_height_profile(-1) { + assert(this->id().invalid()); + assert(this->config.id().invalid()); + assert(this->layer_height_profile.id().invalid()); + assert(rhs.id() != rhs.config.id()); + assert(rhs.id() != rhs.layer_height_profile.id()); + this->assign_copy(std::move(rhs)); + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->layer_height_profile.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->layer_height_profile.id()); + assert(this->id() == rhs.id()); + assert(this->config.id() == rhs.config.id()); + assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); + } + ModelObject& operator=(const ModelObject &rhs) { + this->assign_copy(rhs); + m_model = rhs.m_model; + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->layer_height_profile.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->layer_height_profile.id()); + assert(this->id() == rhs.id()); + assert(this->config.id() == rhs.config.id()); + assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); + return *this; + } + ModelObject& operator=(ModelObject &&rhs) { + this->assign_copy(std::move(rhs)); + m_model = rhs.m_model; + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->layer_height_profile.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->layer_height_profile.id()); + assert(this->id() == rhs.id()); + assert(this->config.id() == rhs.config.id()); + assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); + return *this; + } + void set_new_unique_id() { + ObjectBase::set_new_unique_id(); + this->config.set_new_unique_id(); + this->layer_height_profile.set_new_unique_id(); + } + + OBJECTBASE_DERIVED_COPY_MOVE_CLONE(ModelObject) + + // Parent object, owning this ModelObject. Set to nullptr here, so the macros above will have it initialized. + Model *m_model = nullptr; + + // Bounding box, cached. + mutable BoundingBoxf3 m_bounding_box; + mutable bool m_bounding_box_valid; + mutable BoundingBoxf3 m_raw_bounding_box; + mutable bool m_raw_bounding_box_valid; + mutable BoundingBoxf3 m_raw_mesh_bounding_box; + mutable bool m_raw_mesh_bounding_box_valid; + + // Called by Print::apply() to set the model pointer after making a copy. + friend class Print; + friend class SLAPrint; + void set_model(Model *model) { m_model = model; } + + // Undo / Redo through the cereal serialization library + friend class cereal::access; + friend class UndoRedo::StackImpl; + // Used for deserialization -> Don't allocate any IDs for the ModelObject or its config. + ModelObject() : + ObjectBase(-1), config(-1), layer_height_profile(-1), + m_model(nullptr), m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) { + assert(this->id().invalid()); + assert(this->config.id().invalid()); + assert(this->layer_height_profile.id().invalid()); + } + template void serialize(Archive &ar) { + ar(cereal::base_class(this)); + Internal::StaticSerializationWrapper config_wrapper(config); + Internal::StaticSerializationWrapper layer_heigth_profile_wrapper(layer_height_profile); + ar(name, input_file, instances, volumes, config_wrapper, layer_config_ranges, layer_heigth_profile_wrapper, + sla_support_points, sla_points_status, sla_drain_holes, printable, origin_translation, + m_bounding_box, m_bounding_box_valid, m_raw_bounding_box, m_raw_bounding_box_valid, m_raw_mesh_bounding_box, m_raw_mesh_bounding_box_valid, + cut_connectors, cut_id); + } + + // Called by Print::validate() from the UI thread. + unsigned int update_instances_print_volume_state(const BuildVolume &build_volume); +}; + +enum class EnforcerBlockerType : int8_t { + // Maximum is 3. The value is serialized in TriangleSelector into 2 bits. + NONE = 0, + ENFORCER = 1, + BLOCKER = 2, + // Maximum is 15. The value is serialized in TriangleSelector into 6 bits using a 2 bit prefix code. + Extruder1 = ENFORCER, + Extruder2 = BLOCKER, + Extruder3, + Extruder4, + Extruder5, + Extruder6, + Extruder7, + Extruder8, + Extruder9, + Extruder10, + Extruder11, + Extruder12, + Extruder13, + Extruder14, + Extruder15, +}; + +enum class ConversionType : int { + CONV_TO_INCH, + CONV_FROM_INCH, + CONV_TO_METER, + CONV_FROM_METER, +}; + +class FacetsAnnotation final : public ObjectWithTimestamp { +public: + // Assign the content if the timestamp differs, don't assign an ObjectID. + void assign(const FacetsAnnotation& rhs) { if (! this->timestamp_matches(rhs)) { m_data = rhs.m_data; this->copy_timestamp(rhs); } } + void assign(FacetsAnnotation&& rhs) { if (! this->timestamp_matches(rhs)) { m_data = std::move(rhs.m_data); this->copy_timestamp(rhs); } } + const std::pair>, std::vector>& get_data() const throw() { return m_data; } + bool set(const TriangleSelector& selector); + indexed_triangle_set get_facets(const ModelVolume& mv, EnforcerBlockerType type) const; + indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const; + bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const; + bool empty() const { return m_data.first.empty(); } + + // Following method clears the config and increases its timestamp, so the deleted + // state is considered changed from perspective of the undo/redo stack. + void reset(); + + // Serialize triangle into string, for serialization into 3MF/AMF. + std::string get_triangle_as_string(int i) const; + + // Before deserialization, reserve space for n_triangles. + void reserve(int n_triangles) { m_data.first.reserve(n_triangles); } + // Deserialize triangles one by one, with strictly increasing triangle_id. + void set_triangle_from_string(int triangle_id, const std::string& str); + // After deserializing the last triangle, shrink data to fit. + void shrink_to_fit() { m_data.first.shrink_to_fit(); m_data.second.shrink_to_fit(); } + +private: + // Constructors to be only called by derived classes. + // Default constructor to assign a unique ID. + explicit FacetsAnnotation() = default; + // Constructor with ignored int parameter to assign an invalid ID, to be replaced + // by an existing ID copied from elsewhere. + explicit FacetsAnnotation(int) : ObjectWithTimestamp(-1) {} + // Copy constructor copies the ID. + explicit FacetsAnnotation(const FacetsAnnotation &rhs) = default; + // Move constructor copies the ID. + explicit FacetsAnnotation(FacetsAnnotation &&rhs) = default; + + // called by ModelVolume::assign_copy() + FacetsAnnotation& operator=(const FacetsAnnotation &rhs) = default; + FacetsAnnotation& operator=(FacetsAnnotation &&rhs) = default; + + friend class cereal::access; + friend class UndoRedo::StackImpl; + + template void serialize(Archive &ar) + { + ar(cereal::base_class(this), m_data); + } + + std::pair>, std::vector> m_data; + + // To access set_new_unique_id() when copy / pasting a ModelVolume. + friend class ModelVolume; +}; + +// An object STL, or a modifier volume, over which a different set of parameters shall be applied. +// ModelVolume instances are owned by a ModelObject. +class ModelVolume final : public ObjectBase +{ +public: + std::string name; + // struct used by reload from disk command to recover data from disk + struct Source + { + std::string input_file; + int object_idx{ -1 }; + int volume_idx{ -1 }; + Vec3d mesh_offset{ Vec3d::Zero() }; + Geometry::Transformation transform; + bool is_converted_from_inches{ false }; + bool is_converted_from_meters{ false }; + bool is_from_builtin_objects{ false }; + + template void serialize(Archive& ar) { + //FIXME Vojtech: Serialize / deserialize only if the Source is set. + // likely testing input_file or object_idx would be sufficient. + ar(input_file, object_idx, volume_idx, mesh_offset, transform, is_converted_from_inches, is_converted_from_meters, is_from_builtin_objects); + } + }; + Source source; + + // struct used by cut command + // It contains information about connetors + struct CutInfo + { + bool is_connector{ false }; + bool is_processed{ true }; + CutConnectorType connector_type{ CutConnectorType::Plug }; + float radius_tolerance{ 0.f };// [0.f : 1.f] + float height_tolerance{ 0.f };// [0.f : 1.f] + + CutInfo() = default; + CutInfo(CutConnectorType type, float rad_tolerance, float h_tolerance, bool processed = false) : + is_connector(true), + is_processed(processed), + connector_type(type), + radius_tolerance(rad_tolerance), + height_tolerance(h_tolerance) + {} + + void set_processed() { is_processed = true; } + void invalidate() { is_connector = false; } + + template inline void serialize(Archive& ar) { + ar(is_connector, is_processed, connector_type, radius_tolerance, height_tolerance); + } + }; + CutInfo cut_info; + + bool is_cut_connector() const { return cut_info.is_processed && cut_info.is_connector; } + void invalidate_cut_info() { cut_info.invalidate(); } + + // The triangular model. + const TriangleMesh& mesh() const { return *m_mesh.get(); } +#if ENABLE_RAYCAST_PICKING + std::shared_ptr mesh_ptr() const { return m_mesh; } +#endif // ENABLE_RAYCAST_PICKING + void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared(mesh); } + void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared(std::move(mesh)); } + void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared(mesh); } + void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared(std::move(mesh)); } + void set_mesh(std::shared_ptr &mesh) { m_mesh = mesh; } + void set_mesh(std::unique_ptr &&mesh) { m_mesh = std::move(mesh); } + void reset_mesh() { m_mesh = std::make_shared(); } + // Configuration parameters specific to an object model geometry or a modifier volume, + // overriding the global Slic3r settings and the ModelObject settings. + ModelConfigObject config; + + // List of mesh facets to be supported/unsupported. + FacetsAnnotation supported_facets; + + // List of seam enforcers/blockers. + FacetsAnnotation seam_facets; + + // List of mesh facets painted for MMU segmentation. + FacetsAnnotation mmu_segmentation_facets; + + // A parent object owning this modifier volume. + ModelObject* get_object() const { return this->object; } + ModelVolumeType type() const { return m_type; } + void set_type(const ModelVolumeType t) { m_type = t; } + bool is_model_part() const { return m_type == ModelVolumeType::MODEL_PART; } + bool is_negative_volume() const { return m_type == ModelVolumeType::NEGATIVE_VOLUME; } + bool is_modifier() const { return m_type == ModelVolumeType::PARAMETER_MODIFIER; } + bool is_support_enforcer() const { return m_type == ModelVolumeType::SUPPORT_ENFORCER; } + bool is_support_blocker() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER; } + bool is_support_modifier() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER || m_type == ModelVolumeType::SUPPORT_ENFORCER; } + t_model_material_id material_id() const { return m_material_id; } + void reset_extra_facets(); + void apply_tolerance(); + void set_material_id(t_model_material_id material_id); + ModelMaterial* material() const; + void set_material(t_model_material_id material_id, const ModelMaterial &material); + // Extract the current extruder ID based on this ModelVolume's config and the parent ModelObject's config. + // Extruder ID is only valid for FFF. Returns -1 for SLA or if the extruder ID is not applicable (support volumes). + int extruder_id() const; + + bool is_splittable() const; + + // Split this volume, append the result to the object owning this volume. + // Return the number of volumes created from this one. + // This is useful to assign different materials to different volumes of an object. + size_t split(unsigned int max_extruders); + void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); } + void translate(const Vec3d& displacement); + void scale(const Vec3d& scaling_factors); + void scale(double x, double y, double z) { scale(Vec3d(x, y, z)); } + void scale(double s) { scale(Vec3d(s, s, s)); } + void rotate(double angle, Axis axis); + void rotate(double angle, const Vec3d& axis); + void mirror(Axis axis); + + // This method could only be called before the meshes of this ModelVolumes are not shared! + void scale_geometry_after_creation(const Vec3f &versor); + void scale_geometry_after_creation(const float scale) { this->scale_geometry_after_creation(Vec3f(scale, scale, scale)); } + + // Translates the mesh and the convex hull so that the origin of their vertices is in the center of this volume's bounding box. + // Attention! This method may only be called just after ModelVolume creation! It must not be called once the TriangleMesh of this ModelVolume is shared! + void center_geometry_after_creation(bool update_source_offset = true); + + void calculate_convex_hull(); + const TriangleMesh& get_convex_hull() const; + const std::shared_ptr& get_convex_hull_shared_ptr() const { return m_convex_hull; } + // Get count of errors in the mesh + int get_repaired_errors_count() const; + + // Helpers for loading / storing into AMF / 3MF files. + static ModelVolumeType type_from_string(const std::string &s); + static std::string type_to_string(const ModelVolumeType t); + + const Geometry::Transformation& get_transformation() const { return m_transformation; } + void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; } +#if ENABLE_WORLD_COORDINATE + void set_transformation(const Transform3d& trafo) { m_transformation.set_matrix(trafo); } + + Vec3d get_offset() const { return m_transformation.get_offset(); } +#else + void set_transformation(const Transform3d &trafo) { m_transformation.set_from_transform(trafo); } + + const Vec3d& get_offset() const { return m_transformation.get_offset(); } +#endif // ENABLE_WORLD_COORDINATE + + double get_offset(Axis axis) const { return m_transformation.get_offset(axis); } + + void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); } + void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_rotation() const { return m_transformation.get_rotation(); } +#else + const Vec3d& get_rotation() const { return m_transformation.get_rotation(); } +#endif // ENABLE_WORLD_COORDINATE + double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); } + + void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); } + void set_rotation(Axis axis, double rotation) { m_transformation.set_rotation(axis, rotation); } + + Vec3d get_scaling_factor() const { return m_transformation.get_scaling_factor(); } + double get_scaling_factor(Axis axis) const { return m_transformation.get_scaling_factor(axis); } + + void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); } + void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_mirror() const { return m_transformation.get_mirror(); } +#else + const Vec3d& get_mirror() const { return m_transformation.get_mirror(); } +#endif // ENABLE_WORLD_COORDINATE + double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); } + bool is_left_handed() const { return m_transformation.is_left_handed(); } + + void set_mirror(const Vec3d& mirror) { m_transformation.set_mirror(mirror); } + void set_mirror(Axis axis, double mirror) { m_transformation.set_mirror(axis, mirror); } + void convert_from_imperial_units(); + void convert_from_meters(); + +#if ENABLE_WORLD_COORDINATE + const Transform3d& get_matrix() const { return m_transformation.get_matrix(); } + Transform3d get_matrix_no_offset() const { return m_transformation.get_matrix_no_offset(); } +#else + const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const { return m_transformation.get_matrix(dont_translate, dont_rotate, dont_scale, dont_mirror); } +#endif // ENABLE_WORLD_COORDINATE + + void set_new_unique_id() { + ObjectBase::set_new_unique_id(); + this->config.set_new_unique_id(); + this->supported_facets.set_new_unique_id(); + this->seam_facets.set_new_unique_id(); + this->mmu_segmentation_facets.set_new_unique_id(); + } + + bool is_fdm_support_painted() const { return !this->supported_facets.empty(); } + bool is_seam_painted() const { return !this->seam_facets.empty(); } + bool is_mm_painted() const { return !this->mmu_segmentation_facets.empty(); } + +protected: + friend class Print; + friend class SLAPrint; + friend class Model; + friend class ModelObject; + friend void model_volume_list_update_supports(ModelObject& model_object_dst, const ModelObject& model_object_new); + + // Copies IDs of both the ModelVolume and its config. + explicit ModelVolume(const ModelVolume &rhs) = default; + void set_model_object(ModelObject *model_object) { object = model_object; } + void assign_new_unique_ids_recursive() override; + void transform_this_mesh(const Transform3d& t, bool fix_left_handed); + void transform_this_mesh(const Matrix3d& m, bool fix_left_handed); + +private: + // Parent object owning this ModelVolume. + ModelObject* object; + // The triangular model. + std::shared_ptr m_mesh; + // Is it an object to be printed, or a modifier volume? + ModelVolumeType m_type; + t_model_material_id m_material_id; + // The convex hull of this model's mesh. + std::shared_ptr m_convex_hull; + Geometry::Transformation m_transformation; + + // flag to optimize the checking if the volume is splittable + // -1 -> is unknown value (before first cheking) + // 0 -> is not splittable + // 1 -> is splittable + mutable int m_is_splittable{ -1 }; + + ModelVolume(ModelObject *object, const TriangleMesh &mesh, ModelVolumeType type = ModelVolumeType::MODEL_PART) : m_mesh(new TriangleMesh(mesh)), m_type(type), object(object) + { + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->supported_facets.id().valid()); + assert(this->seam_facets.id().valid()); + assert(this->mmu_segmentation_facets.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->supported_facets.id()); + assert(this->id() != this->seam_facets.id()); + assert(this->id() != this->mmu_segmentation_facets.id()); + if (mesh.facets_count() > 1) + calculate_convex_hull(); + } + ModelVolume(ModelObject *object, TriangleMesh &&mesh, TriangleMesh &&convex_hull, ModelVolumeType type = ModelVolumeType::MODEL_PART) : + m_mesh(new TriangleMesh(std::move(mesh))), m_convex_hull(new TriangleMesh(std::move(convex_hull))), m_type(type), object(object) { + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->supported_facets.id().valid()); + assert(this->seam_facets.id().valid()); + assert(this->mmu_segmentation_facets.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->supported_facets.id()); + assert(this->id() != this->seam_facets.id()); + assert(this->id() != this->mmu_segmentation_facets.id()); + } + + // Copying an existing volume, therefore this volume will get a copy of the ID assigned. + ModelVolume(ModelObject *object, const ModelVolume &other) : + ObjectBase(other), + name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull), + config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation), + supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets), + cut_info(other.cut_info) + { + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->supported_facets.id().valid()); + assert(this->seam_facets.id().valid()); + assert(this->mmu_segmentation_facets.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->supported_facets.id()); + assert(this->id() != this->seam_facets.id()); + assert(this->id() != this->mmu_segmentation_facets.id()); + assert(this->id() == other.id()); + assert(this->config.id() == other.config.id()); + assert(this->supported_facets.id() == other.supported_facets.id()); + assert(this->seam_facets.id() == other.seam_facets.id()); + assert(this->mmu_segmentation_facets.id() == other.mmu_segmentation_facets.id()); + this->set_material_id(other.material_id()); + } + // Providing a new mesh, therefore this volume will get a new unique ID assigned. + ModelVolume(ModelObject *object, const ModelVolume &other, TriangleMesh &&mesh) : + name(other.name), source(other.source), config(other.config), object(object), m_mesh(new TriangleMesh(std::move(mesh))), m_type(other.m_type), m_transformation(other.m_transformation), + cut_info(other.cut_info) + { + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->supported_facets.id().valid()); + assert(this->seam_facets.id().valid()); + assert(this->mmu_segmentation_facets.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->supported_facets.id()); + assert(this->id() != this->seam_facets.id()); + assert(this->id() != this->mmu_segmentation_facets.id()); + assert(this->id() != other.id()); + assert(this->config.id() == other.config.id()); + this->set_material_id(other.material_id()); + this->config.set_new_unique_id(); + if (m_mesh->facets_count() > 1) + calculate_convex_hull(); + assert(this->config.id().valid()); + assert(this->config.id() != other.config.id()); + assert(this->supported_facets.id() != other.supported_facets.id()); + assert(this->seam_facets.id() != other.seam_facets.id()); + assert(this->mmu_segmentation_facets.id() != other.mmu_segmentation_facets.id()); + assert(this->id() != this->config.id()); + assert(this->supported_facets.empty()); + assert(this->seam_facets.empty()); + assert(this->mmu_segmentation_facets.empty()); + } + + ModelVolume& operator=(ModelVolume &rhs) = delete; + + friend class cereal::access; + friend class UndoRedo::StackImpl; + // Used for deserialization, therefore no IDs are allocated. + ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), object(nullptr) { + assert(this->id().invalid()); + assert(this->config.id().invalid()); + assert(this->supported_facets.id().invalid()); + assert(this->seam_facets.id().invalid()); + assert(this->mmu_segmentation_facets.id().invalid()); + } + template void load(Archive &ar) { + bool has_convex_hull; + ar(name, source, m_mesh, m_type, m_material_id, m_transformation, m_is_splittable, has_convex_hull, cut_info); + cereal::load_by_value(ar, supported_facets); + cereal::load_by_value(ar, seam_facets); + cereal::load_by_value(ar, mmu_segmentation_facets); + cereal::load_by_value(ar, config); + assert(m_mesh); + if (has_convex_hull) { + cereal::load_optional(ar, m_convex_hull); + if (! m_convex_hull && ! m_mesh->empty()) + // The convex hull was released from the Undo / Redo stack to conserve memory. Recalculate it. + this->calculate_convex_hull(); + } else + m_convex_hull.reset(); + } + template void save(Archive &ar) const { + bool has_convex_hull = m_convex_hull.get() != nullptr; + ar(name, source, m_mesh, m_type, m_material_id, m_transformation, m_is_splittable, has_convex_hull, cut_info); + cereal::save_by_value(ar, supported_facets); + cereal::save_by_value(ar, seam_facets); + cereal::save_by_value(ar, mmu_segmentation_facets); + cereal::save_by_value(ar, config); + if (has_convex_hull) + cereal::save_optional(ar, m_convex_hull); + } +}; + +inline void model_volumes_sort_by_id(ModelVolumePtrs &model_volumes) +{ + std::sort(model_volumes.begin(), model_volumes.end(), [](const ModelVolume *l, const ModelVolume *r) { return l->id() < r->id(); }); +} + +inline const ModelVolume* model_volume_find_by_id(const ModelVolumePtrs &model_volumes, const ObjectID id) +{ + auto it = lower_bound_by_predicate(model_volumes.begin(), model_volumes.end(), [id](const ModelVolume *mv) { return mv->id() < id; }); + return it != model_volumes.end() && (*it)->id() == id ? *it : nullptr; +} + +enum ModelInstanceEPrintVolumeState : unsigned char +{ + ModelInstancePVS_Inside, + ModelInstancePVS_Partly_Outside, + ModelInstancePVS_Fully_Outside, + ModelInstanceNum_BedStates +}; + +// A single instance of a ModelObject. +// Knows the affine transformation of an object. +class ModelInstance final : public ObjectBase +{ +private: + Geometry::Transformation m_transformation; + +public: + // flag showing the position of this instance with respect to the print volume (set by Print::validate() using ModelObject::check_instances_print_volume_state()) + ModelInstanceEPrintVolumeState print_volume_state; + // Whether or not this instance is printable + bool printable; + + ModelObject* get_object() const { return this->object; } + + const Geometry::Transformation& get_transformation() const { return m_transformation; } + void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_offset() const { return m_transformation.get_offset(); } +#else + const Vec3d& get_offset() const { return m_transformation.get_offset(); } +#endif // ENABLE_WORLD_COORDINATE + double get_offset(Axis axis) const { return m_transformation.get_offset(axis); } + + void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); } + void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_rotation() const { return m_transformation.get_rotation(); } +#else + const Vec3d& get_rotation() const { return m_transformation.get_rotation(); } +#endif // ENABLE_WORLD_COORDINATE + double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); } + + void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); } + void set_rotation(Axis axis, double rotation) { m_transformation.set_rotation(axis, rotation); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_scaling_factor() const { return m_transformation.get_scaling_factor(); } +#else + const Vec3d& get_scaling_factor() const { return m_transformation.get_scaling_factor(); } +#endif // ENABLE_WORLD_COORDINATE + double get_scaling_factor(Axis axis) const { return m_transformation.get_scaling_factor(axis); } + + void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); } + void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_mirror() const { return m_transformation.get_mirror(); } +#else + const Vec3d& get_mirror() const { return m_transformation.get_mirror(); } +#endif // ENABLE_WORLD_COORDINATE + double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); } + bool is_left_handed() const { return m_transformation.is_left_handed(); } + + void set_mirror(const Vec3d& mirror) { m_transformation.set_mirror(mirror); } + void set_mirror(Axis axis, double mirror) { m_transformation.set_mirror(axis, mirror); } + + // To be called on an external mesh + void transform_mesh(TriangleMesh* mesh, bool dont_translate = false) const; + // Calculate a bounding box of a transformed mesh. To be called on an external mesh. + BoundingBoxf3 transform_mesh_bounding_box(const TriangleMesh& mesh, bool dont_translate = false) const; + // Transform an external bounding box. + BoundingBoxf3 transform_bounding_box(const BoundingBoxf3 &bbox, bool dont_translate = false) const; + // Transform an external vector. + Vec3d transform_vector(const Vec3d& v, bool dont_translate = false) const; + // To be called on an external polygon. It does not translate the polygon, only rotates and scales. + void transform_polygon(Polygon* polygon) const; + +#if ENABLE_WORLD_COORDINATE + const Transform3d& get_matrix() const { return m_transformation.get_matrix(); } + Transform3d get_matrix_no_offset() const { return m_transformation.get_matrix_no_offset(); } +#else + const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const { return m_transformation.get_matrix(dont_translate, dont_rotate, dont_scale, dont_mirror); } +#endif // ENABLE_WORLD_COORDINATE + + bool is_printable() const { return object->printable && printable && (print_volume_state == ModelInstancePVS_Inside); } + + // Getting the input polygon for arrange + arrangement::ArrangePolygon get_arrange_polygon() const; + + // Apply the arrange result on the ModelInstance + void apply_arrange_result(const Vec2d& offs, double rotation) + { + // write the transformation data into the model instance + set_rotation(Z, rotation); + set_offset(X, unscale(offs(X))); + set_offset(Y, unscale(offs(Y))); + this->object->invalidate_bounding_box(); + } + +protected: + friend class Print; + friend class SLAPrint; + friend class Model; + friend class ModelObject; + + explicit ModelInstance(const ModelInstance &rhs) = default; + void set_model_object(ModelObject *model_object) { object = model_object; } + +private: + // Parent object, owning this instance. + ModelObject* object; + + // Constructor, which assigns a new unique ID. + explicit ModelInstance(ModelObject* object) : print_volume_state(ModelInstancePVS_Inside), printable(true), object(object) { assert(this->id().valid()); } + // Constructor, which assigns a new unique ID. + explicit ModelInstance(ModelObject *object, const ModelInstance &other) : + m_transformation(other.m_transformation), print_volume_state(ModelInstancePVS_Inside), printable(other.printable), object(object) { assert(this->id().valid() && this->id() != other.id()); } + + explicit ModelInstance(ModelInstance &&rhs) = delete; + ModelInstance& operator=(const ModelInstance &rhs) = delete; + ModelInstance& operator=(ModelInstance &&rhs) = delete; + + friend class cereal::access; + friend class UndoRedo::StackImpl; + // Used for deserialization, therefore no IDs are allocated. + ModelInstance() : ObjectBase(-1), object(nullptr) { assert(this->id().invalid()); } + template void serialize(Archive &ar) { + ar(m_transformation, print_volume_state, printable); + } +}; + + +class ModelWipeTower final : public ObjectBase +{ +public: + Vec2d position; + double rotation; + +private: + friend class cereal::access; + friend class UndoRedo::StackImpl; + friend class Model; + + // Constructors to be only called by derived classes. + // Default constructor to assign a unique ID. + explicit ModelWipeTower() {} + // Constructor with ignored int parameter to assign an invalid ID, to be replaced + // by an existing ID copied from elsewhere. + explicit ModelWipeTower(int) : ObjectBase(-1) {} + // Copy constructor copies the ID. + explicit ModelWipeTower(const ModelWipeTower &cfg) = default; + + // Disabled methods. + ModelWipeTower(ModelWipeTower &&rhs) = delete; + ModelWipeTower& operator=(const ModelWipeTower &rhs) = delete; + ModelWipeTower& operator=(ModelWipeTower &&rhs) = delete; + + // For serialization / deserialization of ModelWipeTower composed into another class into the Undo / Redo stack as a separate object. + template void serialize(Archive &ar) { ar(position, rotation); } +}; + +// The print bed content. +// Description of a triangular model with multiple materials, multiple instances with various affine transformations +// and with multiple modifier meshes. +// A model groups multiple objects, each object having possibly multiple instances, +// all objects may share mutliple materials. +class Model final : public ObjectBase +{ +public: + // Materials are owned by a model and referenced by objects through t_model_material_id. + // Single material may be shared by multiple models. + ModelMaterialMap materials; + // Objects are owned by a model. Each model may have multiple instances, each instance having its own transformation (shift, scale, rotation). + ModelObjectPtrs objects; + // Wipe tower object. + ModelWipeTower wipe_tower; + + // Extensions for color print + CustomGCode::Info custom_gcode_per_print_z; + + // Default constructor assigns a new ID to the model. + Model() { assert(this->id().valid()); } + ~Model() { this->clear_objects(); this->clear_materials(); } + + /* To be able to return an object from own copy / clone methods. Hopefully the compiler will do the "Copy elision" */ + /* (Omits copy and move(since C++11) constructors, resulting in zero - copy pass - by - value semantics). */ + Model(const Model &rhs) : ObjectBase(-1) { assert(this->id().invalid()); this->assign_copy(rhs); assert(this->id().valid()); assert(this->id() == rhs.id()); } + explicit Model(Model &&rhs) : ObjectBase(-1) { assert(this->id().invalid()); this->assign_copy(std::move(rhs)); assert(this->id().valid()); assert(this->id() == rhs.id()); } + Model& operator=(const Model &rhs) { this->assign_copy(rhs); assert(this->id().valid()); assert(this->id() == rhs.id()); return *this; } + Model& operator=(Model &&rhs) { this->assign_copy(std::move(rhs)); assert(this->id().valid()); assert(this->id() == rhs.id()); return *this; } + + OBJECTBASE_DERIVED_COPY_MOVE_CLONE(Model) + + enum class LoadAttribute : int { + AddDefaultInstances, + CheckVersion + }; + using LoadAttributes = enum_bitmask; + + static Model read_from_file( + const std::string& input_file, + DynamicPrintConfig* config = nullptr, ConfigSubstitutionContext* config_substitutions = nullptr, + LoadAttributes options = LoadAttribute::AddDefaultInstances); + static Model read_from_archive( + const std::string& input_file, + DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, + LoadAttributes options = LoadAttribute::AddDefaultInstances); + + // Add a new ModelObject to this Model, generate a new ID for this ModelObject. + ModelObject* add_object(); + ModelObject* add_object(const char *name, const char *path, const TriangleMesh &mesh); + ModelObject* add_object(const char *name, const char *path, TriangleMesh &&mesh); + ModelObject* add_object(const ModelObject &other); + void delete_object(size_t idx); + bool delete_object(ObjectID id); + bool delete_object(ModelObject* object); + void clear_objects(); + + ModelMaterial* add_material(t_model_material_id material_id); + ModelMaterial* add_material(t_model_material_id material_id, const ModelMaterial &other); + ModelMaterial* get_material(t_model_material_id material_id) { + ModelMaterialMap::iterator i = this->materials.find(material_id); + return (i == this->materials.end()) ? nullptr : i->second; + } + + void delete_material(t_model_material_id material_id); + void clear_materials(); + bool add_default_instances(); + // Returns approximate axis aligned bounding box of this model + BoundingBoxf3 bounding_box() const; + // Set the print_volume_state of PrintObject::instances, + // return total number of printable objects. + unsigned int update_print_volume_state(const BuildVolume &build_volume); + // Returns true if any ModelObject was modified. + bool center_instances_around_point(const Vec2d &point); + void translate(coordf_t x, coordf_t y, coordf_t z) { for (ModelObject *o : this->objects) o->translate(x, y, z); } + TriangleMesh mesh() const; + + // Croaks if the duplicated objects do not fit the print bed. + void duplicate_objects_grid(size_t x, size_t y, coordf_t dist); + + bool looks_like_multipart_object() const; + void convert_multipart_object(unsigned int max_extruders); + bool looks_like_imperial_units() const; + void convert_from_imperial_units(bool only_small_volumes); + bool looks_like_saved_in_meters() const; + void convert_from_meters(bool only_small_volumes); + int removed_objects_with_zero_volume(); + + // Ensures that the min z of the model is not negative + void adjust_min_z(); + + void print_info() const { for (const ModelObject *o : this->objects) o->print_info(); } + + // Propose an output file name & path based on the first printable object's name and source input file's path. + std::string propose_export_file_name_and_path() const; + // Propose an output path, replace extension. The new_extension shall contain the initial dot. + std::string propose_export_file_name_and_path(const std::string &new_extension) const; + + // Checks if any of objects is painted using the fdm support painting gizmo. + bool is_fdm_support_painted() const; + // Checks if any of objects is painted using the seam painting gizmo. + bool is_seam_painted() const; + // Checks if any of objects is painted using the multi-material painting gizmo. + bool is_mm_painted() const; + +private: + explicit Model(int) : ObjectBase(-1) { assert(this->id().invalid()); } + void assign_new_unique_ids_recursive(); + void update_links_bottom_up_recursive(); + + friend class cereal::access; + friend class UndoRedo::StackImpl; + template void serialize(Archive &ar) { + Internal::StaticSerializationWrapper wipe_tower_wrapper(wipe_tower); + ar(materials, objects, wipe_tower_wrapper); + } +}; + +ENABLE_ENUM_BITMASK_OPERATORS(Model::LoadAttribute) + +#undef OBJECTBASE_DERIVED_COPY_MOVE_CLONE +#undef OBJECTBASE_DERIVED_PRIVATE_COPY_MOVE + +// Test whether the two models contain the same number of ModelObjects with the same set of IDs +// ordered in the same order. In that case it is not necessary to kill the background processing. +bool model_object_list_equal(const Model &model_old, const Model &model_new); + +// Test whether the new model is just an extension of the old model (new objects were added +// to the end of the original list. In that case it is not necessary to kill the background processing. +bool model_object_list_extended(const Model &model_old, const Model &model_new); + +// Test whether the new ModelObject contains a different set of volumes (or sorted in a different order) +// than the old ModelObject. +bool model_volume_list_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, const ModelVolumeType type); +bool model_volume_list_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, const std::initializer_list &types); + +// Test whether the now ModelObject has newer custom supports data than the old one. +// The function assumes that volumes list is synchronized. +bool model_custom_supports_data_changed(const ModelObject& mo, const ModelObject& mo_new); + +// Test whether the now ModelObject has newer custom seam data than the old one. +// The function assumes that volumes list is synchronized. +bool model_custom_seam_data_changed(const ModelObject& mo, const ModelObject& mo_new); + +// Test whether the now ModelObject has newer MMU segmentation data than the old one. +// The function assumes that volumes list is synchronized. +extern bool model_mmu_segmentation_data_changed(const ModelObject& mo, const ModelObject& mo_new); + +// If the model has multi-part objects, then it is currently not supported by the SLA mode. +// Either the model cannot be loaded, or a SLA printer has to be activated. +bool model_has_multi_part_objects(const Model &model); +// If the model has objects with cut connectrs, then it is currently not supported by the SLA mode. +bool model_has_connectors(const Model& model); +// If the model has advanced features, then it cannot be processed in simple mode. +bool model_has_advanced_features(const Model &model); + +#ifndef NDEBUG +// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique. +void check_model_ids_validity(const Model &model); +void check_model_ids_equal(const Model &model1, const Model &model2); +#endif /* NDEBUG */ + +static const float SINKING_Z_THRESHOLD = -0.001f; +static const double SINKING_MIN_Z_THRESHOLD = 0.05; + +} // namespace Slic3r + +namespace cereal +{ + template struct specialize {}; + template struct specialize {}; +} + +#endif /* slic3r_Model_hpp_ */ diff --git a/src/libslic3r/ObjectID.hpp b/src/libslic3r/ObjectID.hpp index 1030171e7..4f34572d2 100644 --- a/src/libslic3r/ObjectID.hpp +++ b/src/libslic3r/ObjectID.hpp @@ -2,6 +2,7 @@ #define slic3r_ObjectID_hpp_ #include +#include namespace Slic3r { @@ -89,7 +90,9 @@ private: friend class cereal::access; friend class Slic3r::UndoRedo::StackImpl; template void serialize(Archive &ar) { ar(m_id); } +protected: // #vbCHECKME && #ysFIXME ObjectBase(const ObjectID id) : m_id(id) {} +private: template static void load_and_construct(Archive & ar, cereal::construct &construct) { ObjectID id; ar(id); construct(id); } }; @@ -128,6 +131,64 @@ private: template void serialize(Archive &ar) { ar(m_timestamp); } }; +class CutObjectBase : public ObjectBase +{ + // check sum of CutParts in initial Object + size_t m_check_sum{ 1 }; + // connectors count + size_t m_connectors_cnt{ 0 }; + +public: + // Default Constructor to assign an invalid ID + CutObjectBase() : ObjectBase(-1) {} + // Constructor with ignored int parameter to assign an invalid ID, to be replaced + // by an existing ID copied from elsewhere. + CutObjectBase(int) : ObjectBase(-1) {} + // Constructor to initialize full information from 3mf + CutObjectBase(ObjectID id, size_t check_sum, size_t connectors_cnt) : ObjectBase(id), m_check_sum(check_sum), m_connectors_cnt(connectors_cnt) {} + // The class tree will have virtual tables and type information. + virtual ~CutObjectBase() = default; + + bool operator<(const CutObjectBase& other) const { return other.id() > this->id(); } + bool operator==(const CutObjectBase& other) const { return other.id() == this->id(); } + + void copy(const CutObjectBase& rhs) { + this->copy_id(rhs); + this->m_check_sum = rhs.check_sum(); + this->m_connectors_cnt = rhs.connectors_cnt() ; + } + CutObjectBase& operator=(const CutObjectBase& other) { + this->copy(other); + return *this; + } + + void invalidate() { + set_invalid_id(); + m_check_sum = 1; + m_connectors_cnt = 0; + } + + void init() { this->set_new_unique_id(); } + bool has_same_id(const CutObjectBase& rhs) { return this->id() == rhs.id(); } + bool is_equal(const CutObjectBase& rhs) { return this->id() == rhs.id() && + this->check_sum() == rhs.check_sum() && + this->connectors_cnt() == rhs.connectors_cnt() ; } + + size_t check_sum() const { return m_check_sum; } + void set_check_sum(size_t cs) { m_check_sum = cs; } + void increase_check_sum(size_t cnt) { m_check_sum += cnt; } + + size_t connectors_cnt() const { return m_connectors_cnt; } + void increase_connectors_cnt(size_t connectors_cnt) { m_connectors_cnt += connectors_cnt; } + +private: + friend class cereal::access; + template void serialize(Archive& ar) { + ar(cereal::base_class(this)); + ar(m_check_sum, m_connectors_cnt); + } +}; + // Unique object / instance ID for the wipe tower. extern ObjectID wipe_tower_object_id(); extern ObjectID wipe_tower_instance_id(); diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index df820fac9..61d403d95 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -1061,6 +1061,61 @@ indexed_triangle_set its_make_sphere(double radius, double fa) return mesh; } +// Generates mesh for a frustum dowel centered about the origin, using the count of sectors +// Note: This function uses code for sphere generation, but for stackCount = 2; +indexed_triangle_set its_make_frustum_dowel(double radius, double h, int sectorCount) +{ + int stackCount = 2; + float sectorStep = float(2. * M_PI / sectorCount); + float stackStep = float(M_PI / stackCount); + + indexed_triangle_set mesh; + auto& vertices = mesh.vertices; + vertices.reserve((stackCount - 1) * sectorCount + 2); + for (int i = 0; i <= stackCount; ++i) { + // from pi/2 to -pi/2 + double stackAngle = 0.5 * M_PI - stackStep * i; + double xy = radius * cos(stackAngle); + double z = radius * sin(stackAngle); + if (i == 0 || i == stackCount) + vertices.emplace_back(Vec3f(float(xy), 0.f, float(h * sin(stackAngle)))); + else + for (int j = 0; j < sectorCount; ++j) { + // from 0 to 2pi + double sectorAngle = sectorStep * j; + vertices.emplace_back(Vec3d(xy * std::cos(sectorAngle), xy * std::sin(sectorAngle), z).cast()); + } + } + + auto& facets = mesh.indices; + facets.reserve(2 * (stackCount - 1) * sectorCount); + for (int i = 0; i < stackCount; ++i) { + // Beginning of current stack. + int k1 = (i == 0) ? 0 : (1 + (i - 1) * sectorCount); + int k1_first = k1; + // Beginning of next stack. + int k2 = (i == 0) ? 1 : (k1 + sectorCount); + int k2_first = k2; + for (int j = 0; j < sectorCount; ++j) { + // 2 triangles per sector excluding first and last stacks + int k1_next = k1; + int k2_next = k2; + if (i != 0) { + k1_next = (j + 1 == sectorCount) ? k1_first : (k1 + 1); + facets.emplace_back(k1, k2, k1_next); + } + if (i + 1 != stackCount) { + k2_next = (j + 1 == sectorCount) ? k2_first : (k2 + 1); + facets.emplace_back(k1_next, k2, k2_next); + } + k1 = k1_next; + k2 = k2_next; + } + } + + return mesh; +} + indexed_triangle_set its_convex_hull(const std::vector &pts) { std::vector dst_vertices; diff --git a/src/libslic3r/TriangleMesh.hpp b/src/libslic3r/TriangleMesh.hpp index f30776307..ec405073e 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -301,6 +301,7 @@ indexed_triangle_set its_make_cube(double x, double y, double z); indexed_triangle_set its_make_prism(float width, float length, float height); indexed_triangle_set its_make_cylinder(double r, double h, double fa=(2*PI/360)); indexed_triangle_set its_make_cone(double r, double h, double fa=(2*PI/360)); +indexed_triangle_set its_make_frustum_dowel(double r, double h, int sectorCount); indexed_triangle_set its_make_pyramid(float base, float height); indexed_triangle_set its_make_sphere(double radius, double fa); diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index 2285c29a6..054ccd4ea 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -262,9 +262,9 @@ constexpr inline T lerp(const T& a, const T& b, Number t) } template -constexpr inline bool is_approx(Number value, Number test_value) +constexpr inline bool is_approx(Number value, Number test_value, Number precision = EPSILON) { - return std::fabs(double(value) - double(test_value)) < double(EPSILON); + return std::fabs(double(value) - double(test_value)) < double(precision); } // A meta-predicate which is true for integers wider than or equal to coord_t diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 5a5e2bb5a..a25a63c46 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -256,6 +256,8 @@ set(SLIC3R_GUI_SOURCES Utils/WinRegistry.hpp ) +find_package(NanoSVG REQUIRED) + if (APPLE) list(APPEND SLIC3R_GUI_SOURCES Utils/RetinaHelperImpl.mm diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index e674b52d0..bedb3718e 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -33,7 +33,7 @@ wxString double_to_string(double const value, const int max_precision /*= 4*/) // Style_NoTrailingZeroes does not work on OSX. It also does not work correctly with some locales on Windows. // return wxNumberFormatter::ToString(value, max_precision, wxNumberFormatter::Style_NoTrailingZeroes); - wxString s = wxNumberFormatter::ToString(value, value < 0.0001 ? 10 : max_precision, wxNumberFormatter::Style_None); + wxString s = wxNumberFormatter::ToString(value, std::abs(value) < 0.0001 ? 10 : max_precision, wxNumberFormatter::Style_None); // The following code comes from wxNumberFormatter::RemoveTrailingZeroes(wxString& s) // with the exception that here one sets the decimal separator explicitely to dot. diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 80b769a27..25ac88295 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1,7993 +1,8006 @@ -#include "libslic3r/libslic3r.h" -#include "GLCanvas3D.hpp" - -#include - -#include "libslic3r/BuildVolume.hpp" -#include "libslic3r/ClipperUtils.hpp" -#include "libslic3r/PrintConfig.hpp" -#include "libslic3r/GCode/ThumbnailData.hpp" -#include "libslic3r/Geometry/ConvexHull.hpp" -#include "libslic3r/ExtrusionEntity.hpp" -#include "libslic3r/Layer.hpp" -#include "libslic3r/Utils.hpp" -#include "libslic3r/LocalesUtils.hpp" -#include "libslic3r/Technologies.hpp" -#include "libslic3r/Tesselate.hpp" -#include "libslic3r/PresetBundle.hpp" -#include "3DBed.hpp" -#include "3DScene.hpp" -#include "BackgroundSlicingProcess.hpp" -#include "GLShader.hpp" -#include "GUI.hpp" -#include "Tab.hpp" -#include "GUI_Preview.hpp" -#include "OpenGLManager.hpp" -#include "Plater.hpp" -#include "MainFrame.hpp" -#include "GUI_App.hpp" -#include "GUI_ObjectList.hpp" -#include "GUI_ObjectManipulation.hpp" -#include "Mouse3DController.hpp" -#include "I18N.hpp" -#include "NotificationManager.hpp" -#include "format.hpp" - -#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp" -#include "slic3r/Utils/UndoRedo.hpp" - -#if ENABLE_RETINA_GL -#include "slic3r/Utils/RetinaHelper.hpp" -#endif - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -// Print now includes tbb, and tbb includes Windows. This breaks compilation of wxWidgets if included before wx. -#include "libslic3r/Print.hpp" -#include "libslic3r/SLAPrint.hpp" - -#include "wxExtensions.hpp" - -#include -#include - -#include -#include - -#include -#include -#include -#include -#include "DoubleSlider.hpp" - -#include - -static constexpr const float TRACKBALLSIZE = 0.8f; - -#if ENABLE_LEGACY_OPENGL_REMOVAL -static const Slic3r::ColorRGBA DEFAULT_BG_DARK_COLOR = { 0.478f, 0.478f, 0.478f, 1.0f }; -static const Slic3r::ColorRGBA DEFAULT_BG_LIGHT_COLOR = { 0.753f, 0.753f, 0.753f, 1.0f }; -static const Slic3r::ColorRGBA ERROR_BG_DARK_COLOR = { 0.478f, 0.192f, 0.039f, 1.0f }; -static const Slic3r::ColorRGBA ERROR_BG_LIGHT_COLOR = { 0.753f, 0.192f, 0.039f, 1.0f }; -#else -static const Slic3r::ColorRGB DEFAULT_BG_DARK_COLOR = { 0.478f, 0.478f, 0.478f }; -static const Slic3r::ColorRGB DEFAULT_BG_LIGHT_COLOR = { 0.753f, 0.753f, 0.753f }; -static const Slic3r::ColorRGB ERROR_BG_DARK_COLOR = { 0.478f, 0.192f, 0.039f }; -static const Slic3r::ColorRGB ERROR_BG_LIGHT_COLOR = { 0.753f, 0.192f, 0.039f }; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -// Number of floats -static constexpr const size_t MAX_VERTEX_BUFFER_SIZE = 131072 * 6; // 3.15MB -// Reserve size in number of floats. -#if !ENABLE_LEGACY_OPENGL_REMOVAL -static constexpr const size_t VERTEX_BUFFER_RESERVE_SIZE = 131072 * 2; // 1.05MB -// Reserve size in number of floats, maximum sum of all preallocated buffers. -//static constexpr const size_t VERTEX_BUFFER_RESERVE_SIZE_SUM_MAX = 1024 * 1024 * 128 / 4; // 128MB -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - -namespace Slic3r { -namespace GUI { - -#ifdef __WXGTK3__ -// wxGTK3 seems to simulate OSX behavior in regard to HiDPI scaling support. -RetinaHelper::RetinaHelper(wxWindow* window) : m_window(window), m_self(nullptr) {} -RetinaHelper::~RetinaHelper() {} -float RetinaHelper::get_scale_factor() { return float(m_window->GetContentScaleFactor()); } -#endif // __WXGTK3__ - -// Fixed the collision between BuildVolume::Type::Convex and macro Convex defined inside /usr/include/X11/X.h that is included by WxWidgets 3.0. -#if defined(__linux__) && defined(Convex) -#undef Convex -#endif - -GLCanvas3D::LayersEditing::~LayersEditing() -{ - if (m_z_texture_id != 0) { - glsafe(::glDeleteTextures(1, &m_z_texture_id)); - m_z_texture_id = 0; - } - delete m_slicing_parameters; -} - -const float GLCanvas3D::LayersEditing::THICKNESS_BAR_WIDTH = 70.0f; - -void GLCanvas3D::LayersEditing::init() -{ - glsafe(::glGenTextures(1, (GLuint*)&m_z_texture_id)); - glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id)); - if (!OpenGLManager::get_gl_info().is_core_profile() || !OpenGLManager::get_gl_info().is_mesa()) { - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); - } - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1)); - glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); -} - -void GLCanvas3D::LayersEditing::set_config(const DynamicPrintConfig* config) -{ - m_config = config; - delete m_slicing_parameters; - m_slicing_parameters = nullptr; - m_layers_texture.valid = false; -} - -void GLCanvas3D::LayersEditing::select_object(const Model &model, int object_id) -{ - const ModelObject *model_object_new = (object_id >= 0) ? model.objects[object_id] : nullptr; - // Maximum height of an object changes when the object gets rotated or scaled. - // Changing maximum height of an object will invalidate the layer heigth editing profile. - // m_model_object->bounding_box() is cached, therefore it is cheap even if this method is called frequently. - const float new_max_z = (model_object_new == nullptr) ? 0.0f : static_cast(model_object_new->bounding_box().max.z()); - if (m_model_object != model_object_new || this->last_object_id != object_id || m_object_max_z != new_max_z || - (model_object_new != nullptr && m_model_object->id() != model_object_new->id())) { - m_layer_height_profile.clear(); - m_layer_height_profile_modified = false; - delete m_slicing_parameters; - m_slicing_parameters = nullptr; - m_layers_texture.valid = false; - this->last_object_id = object_id; - m_model_object = model_object_new; - m_object_max_z = new_max_z; - } -} - -bool GLCanvas3D::LayersEditing::is_allowed() const -{ - return wxGetApp().get_shader("variable_layer_height") != nullptr && m_z_texture_id > 0; -} - -bool GLCanvas3D::LayersEditing::is_enabled() const -{ - return m_enabled; -} - -void GLCanvas3D::LayersEditing::set_enabled(bool enabled) -{ - m_enabled = is_allowed() && enabled; -} - -float GLCanvas3D::LayersEditing::s_overlay_window_width; - -void GLCanvas3D::LayersEditing::render_overlay(const GLCanvas3D& canvas) -{ - if (!m_enabled) - return; - - const Size& cnv_size = canvas.get_canvas_size(); - - ImGuiWrapper& imgui = *wxGetApp().imgui(); - imgui.set_next_window_pos(static_cast(cnv_size.get_width()) - imgui.get_style_scaling() * THICKNESS_BAR_WIDTH, - static_cast(cnv_size.get_height()), ImGuiCond_Always, 1.0f, 1.0f); - - imgui.begin(_L("Variable layer height"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse); - - imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Left mouse button:")); - ImGui::SameLine(); - imgui.text(_L("Add detail")); - - imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Right mouse button:")); - ImGui::SameLine(); - imgui.text(_L("Remove detail")); - - imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Shift + Left mouse button:")); - ImGui::SameLine(); - imgui.text(_L("Reset to base")); - - imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Shift + Right mouse button:")); - ImGui::SameLine(); - imgui.text(_L("Smoothing")); - - imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Mouse wheel:")); - ImGui::SameLine(); - imgui.text(_L("Increase/decrease edit area")); - - ImGui::Separator(); - if (imgui.button(_L("Adaptive"))) - wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), Event(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, m_adaptive_quality)); - - ImGui::SameLine(); - float text_align = ImGui::GetCursorPosX(); - ImGui::AlignTextToFramePadding(); - imgui.text(_L("Quality / Speed")); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::TextUnformatted(_L("Higher print quality versus higher print speed.").ToUTF8()); - ImGui::EndTooltip(); - } - - ImGui::SameLine(); - float widget_align = ImGui::GetCursorPosX(); - ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f); - m_adaptive_quality = std::clamp(m_adaptive_quality, 0.0f, 1.f); - imgui.slider_float("", &m_adaptive_quality, 0.0f, 1.f, "%.2f"); - - ImGui::Separator(); - if (imgui.button(_L("Smooth"))) - wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), HeightProfileSmoothEvent(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, m_smooth_params)); - - ImGui::SameLine(); - ImGui::SetCursorPosX(text_align); - ImGui::AlignTextToFramePadding(); - imgui.text(_L("Radius")); - ImGui::SameLine(); - ImGui::SetCursorPosX(widget_align); - ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f); - int radius = (int)m_smooth_params.radius; - if (ImGui::SliderInt("##1", &radius, 1, 10)) { - radius = std::clamp(radius, 1, 10); - m_smooth_params.radius = (unsigned int)radius; - } - - ImGui::SetCursorPosX(text_align); - ImGui::AlignTextToFramePadding(); - imgui.text(_L("Keep min")); - ImGui::SameLine(); - if (ImGui::GetCursorPosX() < widget_align) // because of line lenght after localization - ImGui::SetCursorPosX(widget_align); - - ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f); - imgui.checkbox("##2", m_smooth_params.keep_min); - - ImGui::Separator(); - if (imgui.button(_L("Reset"))) - wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), SimpleEvent(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE)); - - GLCanvas3D::LayersEditing::s_overlay_window_width = ImGui::GetWindowSize().x /*+ (float)m_layers_texture.width/4*/; - imgui.end(); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - render_active_object_annotations(canvas); - render_profile(canvas); -#else - const Rect& bar_rect = get_bar_rect_viewport(canvas); - render_active_object_annotations(canvas, bar_rect); - render_profile(bar_rect); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -float GLCanvas3D::LayersEditing::get_cursor_z_relative(const GLCanvas3D& canvas) -{ - const Vec2d mouse_pos = canvas.get_local_mouse_position(); - const Rect& rect = get_bar_rect_screen(canvas); - float x = (float)mouse_pos.x(); - float y = (float)mouse_pos.y(); - float t = rect.get_top(); - float b = rect.get_bottom(); - - return (rect.get_left() <= x && x <= rect.get_right() && t <= y && y <= b) ? - // Inside the bar. - (b - y - 1.0f) / (b - t - 1.0f) : - // Outside the bar. - -1000.0f; -} - -bool GLCanvas3D::LayersEditing::bar_rect_contains(const GLCanvas3D& canvas, float x, float y) -{ - const Rect& rect = get_bar_rect_screen(canvas); - return rect.get_left() <= x && x <= rect.get_right() && rect.get_top() <= y && y <= rect.get_bottom(); -} - -Rect GLCanvas3D::LayersEditing::get_bar_rect_screen(const GLCanvas3D& canvas) -{ - const Size& cnv_size = canvas.get_canvas_size(); - float w = (float)cnv_size.get_width(); - float h = (float)cnv_size.get_height(); - - return { w - thickness_bar_width(canvas), 0.0f, w, h }; -} - -#if !ENABLE_LEGACY_OPENGL_REMOVAL -Rect GLCanvas3D::LayersEditing::get_bar_rect_viewport(const GLCanvas3D& canvas) -{ - const Size& cnv_size = canvas.get_canvas_size(); - float half_w = 0.5f * (float)cnv_size.get_width(); - float half_h = 0.5f * (float)cnv_size.get_height(); - float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom(); - return { (half_w - thickness_bar_width(canvas)) * inv_zoom, half_h * inv_zoom, half_w * inv_zoom, -half_h * inv_zoom }; -} -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - -bool GLCanvas3D::LayersEditing::is_initialized() const -{ - return wxGetApp().get_shader("variable_layer_height") != nullptr; -} - -std::string GLCanvas3D::LayersEditing::get_tooltip(const GLCanvas3D& canvas) const -{ - std::string ret; - if (m_enabled && m_layer_height_profile.size() >= 4) { - float z = get_cursor_z_relative(canvas); - if (z != -1000.0f) { - z *= m_object_max_z; - - float h = 0.0f; - for (size_t i = m_layer_height_profile.size() - 2; i >= 2; i -= 2) { - const float zi = static_cast(m_layer_height_profile[i]); - const float zi_1 = static_cast(m_layer_height_profile[i - 2]); - if (zi_1 <= z && z <= zi) { - float dz = zi - zi_1; - h = (dz != 0.0f) ? static_cast(lerp(m_layer_height_profile[i - 1], m_layer_height_profile[i + 1], (z - zi_1) / dz)) : - static_cast(m_layer_height_profile[i + 1]); - break; - } - } - if (h > 0.0f) - ret = std::to_string(h); - } - } - return ret; -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3D& canvas) -{ - const Size cnv_size = canvas.get_canvas_size(); - const float cnv_width = (float)cnv_size.get_width(); - const float cnv_height = (float)cnv_size.get_height(); - if (cnv_width == 0.0f || cnv_height == 0.0f) - return; - - const float cnv_inv_width = 1.0f / cnv_width; -#else -void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3D& canvas, const Rect& bar_rect) -{ -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_shader("variable_layer_height"); - if (shader == nullptr) - return; - - shader->start_using(); - - shader->set_uniform("z_to_texture_row", float(m_layers_texture.cells - 1) / (float(m_layers_texture.width) * m_object_max_z)); - shader->set_uniform("z_texture_row_to_normalized", 1.0f / (float)m_layers_texture.height); - shader->set_uniform("z_cursor", m_object_max_z * this->get_cursor_z_relative(canvas)); - shader->set_uniform("z_cursor_band_width", band_width); - shader->set_uniform("object_max_z", m_object_max_z); -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->set_uniform("view_model_matrix", Transform3d::Identity()); - shader->set_uniform("projection_matrix", Transform3d::Identity()); - shader->set_uniform("view_normal_matrix", (Matrix3d)Matrix3d::Identity()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); - glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id)); - - // Render the color bar -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_profile.background.is_initialized() || m_profile.old_canvas_width != cnv_width) { - m_profile.old_canvas_width = cnv_width; - m_profile.background.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3T2 }; - init_data.reserve_vertices(4); - init_data.reserve_indices(6); - - // vertices - const float l = 1.0f - 2.0f * THICKNESS_BAR_WIDTH * cnv_inv_width; - const float r = 1.0f; - const float t = 1.0f; - const float b = -1.0f; - init_data.add_vertex(Vec3f(l, b, 0.0f), Vec3f::UnitZ(), Vec2f(0.0f, 0.0f)); - init_data.add_vertex(Vec3f(r, b, 0.0f), Vec3f::UnitZ(), Vec2f(1.0f, 0.0f)); - init_data.add_vertex(Vec3f(r, t, 0.0f), Vec3f::UnitZ(), Vec2f(1.0f, 1.0f)); - init_data.add_vertex(Vec3f(l, t, 0.0f), Vec3f::UnitZ(), Vec2f(0.0f, 1.0f)); - - // indices - init_data.add_triangle(0, 1, 2); - init_data.add_triangle(2, 3, 0); - - m_profile.background.init_from(std::move(init_data)); - } - - m_profile.background.render(); -#else - const float l = bar_rect.get_left(); - const float r = bar_rect.get_right(); - const float t = bar_rect.get_top(); - const float b = bar_rect.get_bottom(); - - ::glBegin(GL_QUADS); - ::glNormal3f(0.0f, 0.0f, 1.0f); - ::glTexCoord2f(0.0f, 0.0f); ::glVertex2f(l, b); - ::glTexCoord2f(1.0f, 0.0f); ::glVertex2f(r, b); - ::glTexCoord2f(1.0f, 1.0f); ::glVertex2f(r, t); - ::glTexCoord2f(0.0f, 1.0f); ::glVertex2f(l, t); - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); - - shader->stop_using(); -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLCanvas3D::LayersEditing::render_profile(const GLCanvas3D& canvas) -#else -void GLCanvas3D::LayersEditing::render_profile(const Rect& bar_rect) -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ - //FIXME show some kind of legend. - - if (!m_slicing_parameters) - return; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Size cnv_size = canvas.get_canvas_size(); - const float cnv_width = (float)cnv_size.get_width(); - const float cnv_height = (float)cnv_size.get_height(); - if (cnv_width == 0.0f || cnv_height == 0.0f) - return; - - // Make the vertical bar a bit wider so the layer height curve does not touch the edge of the bar region. - const float scale_x = THICKNESS_BAR_WIDTH / float(1.12 * m_slicing_parameters->max_layer_height); - const float scale_y = cnv_height / m_object_max_z; - - const float cnv_inv_width = 1.0f / cnv_width; - const float cnv_inv_height = 1.0f / cnv_height; -#else - // Make the vertical bar a bit wider so the layer height curve does not touch the edge of the bar region. - const float scale_x = bar_rect.get_width() / float(1.12 * m_slicing_parameters->max_layer_height); - const float scale_y = bar_rect.get_height() / m_object_max_z; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_LEGACY_OPENGL_REMOVAL - // Baseline - if (!m_profile.baseline.is_initialized() || m_profile.old_layer_height_profile != m_layer_height_profile) { - m_profile.baseline.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P2 }; - init_data.color = ColorRGBA::BLACK(); - init_data.reserve_vertices(2); - init_data.reserve_indices(2); - - // vertices - const float axis_x = 2.0f * ((cnv_width - THICKNESS_BAR_WIDTH + float(m_slicing_parameters->layer_height) * scale_x) * cnv_inv_width - 0.5f); - init_data.add_vertex(Vec2f(axis_x, -1.0f)); - init_data.add_vertex(Vec2f(axis_x, 1.0f)); - - // indices - init_data.add_line(0, 1); - - m_profile.baseline.init_from(std::move(init_data)); - } - - if (!m_profile.profile.is_initialized() || m_profile.old_layer_height_profile != m_layer_height_profile) { - m_profile.old_layer_height_profile = m_layer_height_profile; - m_profile.profile.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::LineStrip, GLModel::Geometry::EVertexLayout::P2 }; - init_data.color = ColorRGBA::BLUE(); - init_data.reserve_vertices(m_layer_height_profile.size() / 2); - init_data.reserve_indices(m_layer_height_profile.size() / 2); - - // vertices + indices - for (unsigned int i = 0; i < (unsigned int)m_layer_height_profile.size(); i += 2) { - init_data.add_vertex(Vec2f(2.0f * ((cnv_width - THICKNESS_BAR_WIDTH + float(m_layer_height_profile[i + 1]) * scale_x) * cnv_inv_width - 0.5f), - 2.0f * (float(m_layer_height_profile[i]) * scale_y * cnv_inv_height - 0.5))); - init_data.add_index(i / 2); - } - - m_profile.profile.init_from(std::move(init_data)); - } - -#if ENABLE_GL_CORE_PROFILE - GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); -#else - GLShaderProgram* shader = wxGetApp().get_shader("flat"); -#endif // ENABLE_GL_CORE_PROFILE - if (shader != nullptr) { - shader->start_using(); - shader->set_uniform("view_model_matrix", Transform3d::Identity()); - shader->set_uniform("projection_matrix", Transform3d::Identity()); -#if ENABLE_GL_CORE_PROFILE - const std::array& viewport = wxGetApp().plater()->get_camera().get_viewport(); - shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); - shader->set_uniform("width", 0.25f); - shader->set_uniform("gap_size", 0.0f); -#endif // ENABLE_GL_CORE_PROFILE - m_profile.baseline.render(); - m_profile.profile.render(); - shader->stop_using(); - } -#else - const float x = bar_rect.get_left() + float(m_slicing_parameters->layer_height) * scale_x; - - // Baseline - glsafe(::glColor3f(0.0f, 0.0f, 0.0f)); - ::glBegin(GL_LINE_STRIP); - ::glVertex2f(x, bar_rect.get_bottom()); - ::glVertex2f(x, bar_rect.get_top()); - glsafe(::glEnd()); - - // Curve - glsafe(::glColor3f(0.0f, 0.0f, 1.0f)); - ::glBegin(GL_LINE_STRIP); - for (unsigned int i = 0; i < m_layer_height_profile.size(); i += 2) - ::glVertex2f(bar_rect.get_left() + (float)m_layer_height_profile[i + 1] * scale_x, bar_rect.get_bottom() + (float)m_layer_height_profile[i] * scale_y); - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -void GLCanvas3D::LayersEditing::render_volumes(const GLCanvas3D& canvas, const GLVolumeCollection& volumes) -{ - assert(this->is_allowed()); - assert(this->last_object_id != -1); - - GLShaderProgram* current_shader = wxGetApp().get_current_shader(); - ScopeGuard guard([current_shader]() { if (current_shader != nullptr) current_shader->start_using(); }); - if (current_shader != nullptr) - current_shader->stop_using(); - - GLShaderProgram* shader = wxGetApp().get_shader("variable_layer_height"); - if (shader == nullptr) - return; - - shader->start_using(); - - generate_layer_height_texture(); - - // Uniforms were resolved, go ahead using the layer editing shader. - shader->set_uniform("z_to_texture_row", float(m_layers_texture.cells - 1) / (float(m_layers_texture.width) * float(m_object_max_z))); - shader->set_uniform("z_texture_row_to_normalized", 1.0f / float(m_layers_texture.height)); - shader->set_uniform("z_cursor", float(m_object_max_z) * float(this->get_cursor_z_relative(canvas))); - shader->set_uniform("z_cursor_band_width", float(this->band_width)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - // Initialize the layer height texture mapping. - const GLsizei w = (GLsizei)m_layers_texture.width; - const GLsizei h = (GLsizei)m_layers_texture.height; - const GLsizei half_w = w / 2; - const GLsizei half_h = h / 2; - glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); - glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id)); - glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0)); - glsafe(::glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, half_w, half_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0)); - glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data())); - glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, half_w, half_h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data() + m_layers_texture.width * m_layers_texture.height * 4)); - for (GLVolume* glvolume : volumes.volumes) { - // Render the object using the layer editing shader and texture. - if (!glvolume->is_active || glvolume->composite_id.object_id != this->last_object_id || glvolume->is_modifier) - continue; - - shader->set_uniform("volume_world_matrix", glvolume->world_matrix()); - shader->set_uniform("object_max_z", 0.0f); -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Transform3d& view_matrix = camera.get_view_matrix(); - const Transform3d model_matrix = glvolume->world_matrix(); - shader->set_uniform("view_model_matrix", view_matrix * model_matrix); - const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); - shader->set_uniform("view_normal_matrix", view_normal_matrix); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - glvolume->render(); - } - // Revert back to the previous shader. - glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); -} - -void GLCanvas3D::LayersEditing::adjust_layer_height_profile() -{ - this->update_slicing_parameters(); - PrintObject::update_layer_height_profile(*m_model_object, *m_slicing_parameters, m_layer_height_profile); - Slic3r::adjust_layer_height_profile(*m_slicing_parameters, m_layer_height_profile, this->last_z, this->strength, this->band_width, this->last_action); - m_layer_height_profile_modified = true; - m_layers_texture.valid = false; -} - -void GLCanvas3D::LayersEditing::reset_layer_height_profile(GLCanvas3D& canvas) -{ - const_cast(m_model_object)->layer_height_profile.clear(); - m_layer_height_profile.clear(); - m_layers_texture.valid = false; - canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - wxGetApp().obj_list()->update_info_items(last_object_id); -} - -void GLCanvas3D::LayersEditing::adaptive_layer_height_profile(GLCanvas3D& canvas, float quality_factor) -{ - this->update_slicing_parameters(); - m_layer_height_profile = layer_height_profile_adaptive(*m_slicing_parameters, *m_model_object, quality_factor); - const_cast(m_model_object)->layer_height_profile.set(m_layer_height_profile); - m_layers_texture.valid = false; - canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - wxGetApp().obj_list()->update_info_items(last_object_id); -} - -void GLCanvas3D::LayersEditing::smooth_layer_height_profile(GLCanvas3D& canvas, const HeightProfileSmoothingParams& smoothing_params) -{ - this->update_slicing_parameters(); - m_layer_height_profile = smooth_height_profile(m_layer_height_profile, *m_slicing_parameters, smoothing_params); - const_cast(m_model_object)->layer_height_profile.set(m_layer_height_profile); - m_layers_texture.valid = false; - canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - wxGetApp().obj_list()->update_info_items(last_object_id); -} - -void GLCanvas3D::LayersEditing::generate_layer_height_texture() -{ - this->update_slicing_parameters(); - // Always try to update the layer height profile. - bool update = ! m_layers_texture.valid; - if (PrintObject::update_layer_height_profile(*m_model_object, *m_slicing_parameters, m_layer_height_profile)) { - // Initialized to the default value. - m_layer_height_profile_modified = false; - update = true; - } - // Update if the layer height profile was changed, or when the texture is not valid. - if (! update && ! m_layers_texture.data.empty() && m_layers_texture.cells > 0) - // Texture is valid, don't update. - return; - - if (m_layers_texture.data.empty()) { - m_layers_texture.width = 1024; - m_layers_texture.height = 1024; - m_layers_texture.levels = 2; - m_layers_texture.data.assign(m_layers_texture.width * m_layers_texture.height * 5, 0); - } - - bool level_of_detail_2nd_level = true; - m_layers_texture.cells = Slic3r::generate_layer_height_texture( - *m_slicing_parameters, - Slic3r::generate_object_layers(*m_slicing_parameters, m_layer_height_profile), - m_layers_texture.data.data(), m_layers_texture.height, m_layers_texture.width, level_of_detail_2nd_level); - m_layers_texture.valid = true; -} - -void GLCanvas3D::LayersEditing::accept_changes(GLCanvas3D& canvas) -{ - if (last_object_id >= 0) { - if (m_layer_height_profile_modified) { - wxGetApp().plater()->take_snapshot(_L("Variable layer height - Manual edit")); - const_cast(m_model_object)->layer_height_profile.set(m_layer_height_profile); - canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - wxGetApp().obj_list()->update_info_items(last_object_id); - } - } - m_layer_height_profile_modified = false; -} - -void GLCanvas3D::LayersEditing::update_slicing_parameters() -{ - if (m_slicing_parameters == nullptr) { - m_slicing_parameters = new SlicingParameters(); - *m_slicing_parameters = PrintObject::slicing_parameters(*m_config, *m_model_object, m_object_max_z); - } -} - -float GLCanvas3D::LayersEditing::thickness_bar_width(const GLCanvas3D &canvas) -{ - return -#if ENABLE_RETINA_GL - canvas.get_canvas_size().get_scale_factor() -#else - canvas.get_wxglcanvas()->GetContentScaleFactor() -#endif - * THICKNESS_BAR_WIDTH; -} - - -const Point GLCanvas3D::Mouse::Drag::Invalid_2D_Point(INT_MAX, INT_MAX); -const Vec3d GLCanvas3D::Mouse::Drag::Invalid_3D_Point(DBL_MAX, DBL_MAX, DBL_MAX); -const int GLCanvas3D::Mouse::Drag::MoveThresholdPx = 5; - -GLCanvas3D::Mouse::Drag::Drag() - : start_position_2D(Invalid_2D_Point) - , start_position_3D(Invalid_3D_Point) - , move_volume_idx(-1) - , move_requires_threshold(false) - , move_start_threshold_position_2D(Invalid_2D_Point) -{ -} - -GLCanvas3D::Mouse::Mouse() - : dragging(false) - , position(DBL_MAX, DBL_MAX) - , scene_position(DBL_MAX, DBL_MAX, DBL_MAX) - , ignore_left_up(false) -{ -} - -void GLCanvas3D::Labels::render(const std::vector& sorted_instances) const -{ - if (!m_enabled || !is_shown()) - return; - - const Camera& camera = wxGetApp().plater()->get_camera(); - const Model* model = m_canvas.get_model(); - if (model == nullptr) - return; - - Transform3d world_to_eye = camera.get_view_matrix(); - Transform3d world_to_screen = camera.get_projection_matrix() * world_to_eye; - const std::array& viewport = camera.get_viewport(); - - struct Owner - { - int obj_idx; - int inst_idx; - size_t model_instance_id; - BoundingBoxf3 world_box; - double eye_center_z; - std::string title; - std::string label; - std::string print_order; - bool selected; - }; - - // collect owners world bounding boxes and data from volumes - std::vector owners; - const GLVolumeCollection& volumes = m_canvas.get_volumes(); - for (const GLVolume* volume : volumes.volumes) { - int obj_idx = volume->object_idx(); - if (0 <= obj_idx && obj_idx < (int)model->objects.size()) { - int inst_idx = volume->instance_idx(); - std::vector::iterator it = std::find_if(owners.begin(), owners.end(), [obj_idx, inst_idx](const Owner& owner) { - return (owner.obj_idx == obj_idx) && (owner.inst_idx == inst_idx); - }); - if (it != owners.end()) { - it->world_box.merge(volume->transformed_bounding_box()); - it->selected &= volume->selected; - } else { - const ModelObject* model_object = model->objects[obj_idx]; - Owner owner; - owner.obj_idx = obj_idx; - owner.inst_idx = inst_idx; - owner.model_instance_id = model_object->instances[inst_idx]->id().id; - owner.world_box = volume->transformed_bounding_box(); - owner.title = "object" + std::to_string(obj_idx) + "_inst##" + std::to_string(inst_idx); - owner.label = model_object->name; - if (model_object->instances.size() > 1) - owner.label += " (" + std::to_string(inst_idx + 1) + ")"; - owner.selected = volume->selected; - owners.emplace_back(owner); - } - } - } - - // updates print order strings - if (sorted_instances.size() > 1) { - for (size_t i = 0; i < sorted_instances.size(); ++i) { - size_t id = sorted_instances[i]->id().id; - std::vector::iterator it = std::find_if(owners.begin(), owners.end(), [id](const Owner& owner) { - return owner.model_instance_id == id; - }); - if (it != owners.end()) - it->print_order = std::string((_(L("Seq."))).ToUTF8()) + "#: " + std::to_string(i + 1); - } - } - - // calculate eye bounding boxes center zs - for (Owner& owner : owners) { - owner.eye_center_z = (world_to_eye * owner.world_box.center())(2); - } - - // sort owners by center eye zs and selection - std::sort(owners.begin(), owners.end(), [](const Owner& owner1, const Owner& owner2) { - if (!owner1.selected && owner2.selected) - return true; - else if (owner1.selected && !owner2.selected) - return false; - else - return (owner1.eye_center_z < owner2.eye_center_z); - }); - - ImGuiWrapper& imgui = *wxGetApp().imgui(); - - // render info windows - for (const Owner& owner : owners) { - Vec3d screen_box_center = world_to_screen * owner.world_box.center(); - float x = 0.0f; - float y = 0.0f; - if (camera.get_type() == Camera::EType::Perspective) { - x = (0.5f + 0.001f * 0.5f * (float)screen_box_center(0)) * viewport[2]; - y = (0.5f - 0.001f * 0.5f * (float)screen_box_center(1)) * viewport[3]; - } else { - x = (0.5f + 0.5f * (float)screen_box_center(0)) * viewport[2]; - y = (0.5f - 0.5f * (float)screen_box_center(1)) * viewport[3]; - } - - if (x < 0.0f || viewport[2] < x || y < 0.0f || viewport[3] < y) - continue; - - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, owner.selected ? 3.0f : 1.5f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleColor(ImGuiCol_Border, owner.selected ? ImVec4(0.757f, 0.404f, 0.216f, 1.0f) : ImVec4(0.75f, 0.75f, 0.75f, 1.0f)); - imgui.set_next_window_pos(x, y, ImGuiCond_Always, 0.5f, 0.5f); - imgui.begin(owner.title, ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove); - ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); - float win_w = ImGui::GetWindowWidth(); - float label_len = ImGui::CalcTextSize(owner.label.c_str()).x; - ImGui::SetCursorPosX(0.5f * (win_w - label_len)); - ImGui::AlignTextToFramePadding(); - imgui.text(owner.label); - - if (!owner.print_order.empty()) { - ImGui::Separator(); - float po_len = ImGui::CalcTextSize(owner.print_order.c_str()).x; - ImGui::SetCursorPosX(0.5f * (win_w - po_len)); - ImGui::AlignTextToFramePadding(); - imgui.text(owner.print_order); - } - - // force re-render while the windows gets to its final size (it takes several frames) - if (ImGui::GetWindowContentRegionWidth() + 2.0f * ImGui::GetStyle().WindowPadding.x != ImGui::CalcWindowNextAutoFitSize(ImGui::GetCurrentWindow()).x) - imgui.set_requires_extra_frame(); - - imgui.end(); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(2); - } -} - -void GLCanvas3D::Tooltip::set_text(const std::string& text) -{ - // If the mouse is inside an ImGUI dialog, then the tooltip is suppressed. - m_text = m_in_imgui ? std::string() : text; -} - -void GLCanvas3D::Tooltip::render(const Vec2d& mouse_position, GLCanvas3D& canvas) -{ - static ImVec2 size(0.0f, 0.0f); - - auto validate_position = [](const Vec2d& position, const GLCanvas3D& canvas, const ImVec2& wnd_size) { - auto calc_cursor_height = []() { - float ret = 16.0f; -#ifdef _WIN32 - // see: https://forums.codeguru.com/showthread.php?449040-get-the-system-current-cursor-size - // this code is not perfect because it returns a maximum height equal to 31 even if the cursor bitmap shown on screen is bigger - // but at least it gives the same result as wxWidgets in the settings tabs - ICONINFO ii; - if (::GetIconInfo((HICON)GetCursor(), &ii) != 0) { - BITMAP bitmap; - ::GetObject(ii.hbmMask, sizeof(BITMAP), &bitmap); - int width = bitmap.bmWidth; - int height = (ii.hbmColor == nullptr) ? bitmap.bmHeight / 2 : bitmap.bmHeight; - HDC dc = ::CreateCompatibleDC(nullptr); - if (dc != nullptr) { - if (::SelectObject(dc, ii.hbmMask) != nullptr) { - for (int i = 0; i < width; ++i) { - for (int j = 0; j < height; ++j) { - if (::GetPixel(dc, i, j) != RGB(255, 255, 255)) { - if (ret < float(j)) - ret = float(j); - } - } - } - ::DeleteDC(dc); - } - } - ::DeleteObject(ii.hbmColor); - ::DeleteObject(ii.hbmMask); - } -#endif // _WIN32 - return ret; - }; - - const Size cnv_size = canvas.get_canvas_size(); - const float x = std::clamp(float(position.x()), 0.0f, float(cnv_size.get_width()) - wnd_size.x); - const float y = std::clamp(float(position.y()) + calc_cursor_height(), 0.0f, float(cnv_size.get_height()) - wnd_size.y); - return Vec2f(x, y); - }; - - if (m_text.empty()) { - m_start_time = std::chrono::steady_clock::now(); - return; - } - - // draw the tooltip as hidden until the delay is expired - // use a value of alpha slightly different from 0.0f because newer imgui does not calculate properly the window size if alpha == 0.0f - const float alpha = (std::chrono::duration_cast(std::chrono::steady_clock::now() - m_start_time).count() < 500) ? 0.01f : 1.0f; - - const Vec2f position = validate_position(mouse_position, canvas, size); - - ImGuiWrapper& imgui = *wxGetApp().imgui(); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha); - imgui.set_next_window_pos(position.x(), position.y(), ImGuiCond_Always, 0.0f, 0.0f); - - imgui.begin(wxString("canvas_tooltip"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoFocusOnAppearing); - ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); - ImGui::TextUnformatted(m_text.c_str()); - - // force re-render while the windows gets to its final size (it may take several frames) or while hidden - if (alpha < 1.0f || ImGui::GetWindowContentRegionWidth() + 2.0f * ImGui::GetStyle().WindowPadding.x != ImGui::CalcWindowNextAutoFitSize(ImGui::GetCurrentWindow()).x) - imgui.set_requires_extra_frame(); - - size = ImGui::GetWindowSize(); - - imgui.end(); - ImGui::PopStyleVar(2); -} - -void GLCanvas3D::SequentialPrintClearance::set_polygons(const Polygons& polygons) -{ - m_perimeter.reset(); - m_fill.reset(); - if (polygons.empty()) - return; - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - size_t triangles_count = 0; - for (const Polygon& poly : polygons) { - triangles_count += poly.points.size() - 2; - } - const size_t vertices_count = 3 * triangles_count; -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - if (m_render_fill) { - GLModel::Geometry fill_data; -#if ENABLE_LEGACY_OPENGL_REMOVAL - fill_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; - fill_data.color = { 0.3333f, 0.0f, 0.0f, 0.5f }; - - // vertices + indices - const ExPolygons polygons_union = union_ex(polygons); - unsigned int vertices_counter = 0; - for (const ExPolygon& poly : polygons_union) { - const std::vector triangulation = triangulate_expolygon_3d(poly); - fill_data.reserve_vertices(fill_data.vertices_count() + triangulation.size()); - fill_data.reserve_indices(fill_data.indices_count() + triangulation.size()); - for (const Vec3d& v : triangulation) { - fill_data.add_vertex((Vec3f)(v.cast() + 0.0125f * Vec3f::UnitZ())); // add a small positive z to avoid z-fighting - ++vertices_counter; - if (vertices_counter % 3 == 0) - fill_data.add_triangle(vertices_counter - 3, vertices_counter - 2, vertices_counter - 1); - } - } - - m_fill.init_from(std::move(fill_data)); -#else - GLModel::Geometry::Entity entity; - entity.type = GLModel::EPrimitiveType::Triangles; - entity.color = { 0.3333f, 0.0f, 0.0f, 0.5f }; - entity.positions.reserve(vertices_count); - entity.normals.reserve(vertices_count); - entity.indices.reserve(vertices_count); - - const ExPolygons polygons_union = union_ex(polygons); - for (const ExPolygon& poly : polygons_union) { - const std::vector triangulation = triangulate_expolygon_3d(poly); - for (const Vec3d& v : triangulation) { - entity.positions.emplace_back(v.cast() + Vec3f(0.0f, 0.0f, 0.0125f)); // add a small positive z to avoid z-fighting - entity.normals.emplace_back(Vec3f::UnitZ()); - const size_t positions_count = entity.positions.size(); - if (positions_count % 3 == 0) { - entity.indices.emplace_back(positions_count - 3); - entity.indices.emplace_back(positions_count - 2); - entity.indices.emplace_back(positions_count - 1); - } - } - } - - fill_data.entities.emplace_back(entity); - m_fill.init_from(fill_data); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_perimeter.init_from(polygons, 0.025f); // add a small positive z to avoid z-fighting -#else - GLModel::Geometry perimeter_data; - for (const Polygon& poly : polygons) { - GLModel::Geometry::Entity ent; - ent.type = GLModel::EPrimitiveType::LineLoop; - ent.positions.reserve(poly.points.size()); - ent.indices.reserve(poly.points.size()); - unsigned int id_count = 0; - for (const Point& p : poly.points) { - ent.positions.emplace_back(unscale(p.x()), unscale(p.y()), 0.025f); // add a small positive z to avoid z-fighting - ent.normals.emplace_back(Vec3f::UnitZ()); - ent.indices.emplace_back(id_count++); - } - - perimeter_data.entities.emplace_back(ent); - } - - m_perimeter.init_from(perimeter_data); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -void GLCanvas3D::SequentialPrintClearance::render() -{ - const ColorRGBA FILL_COLOR = { 1.0f, 0.0f, 0.0f, 0.5f }; - const ColorRGBA NO_FILL_COLOR = { 1.0f, 1.0f, 1.0f, 0.75f }; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_shader("flat"); -#else - GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - if (shader == nullptr) - return; - - shader->start_using(); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("view_model_matrix", camera.get_view_matrix()); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glDisable(GL_CULL_FACE)); - glsafe(::glEnable(GL_BLEND)); - glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_perimeter.set_color(m_render_fill ? FILL_COLOR : NO_FILL_COLOR); -#else - m_perimeter.set_color(-1, m_render_fill ? FILL_COLOR : NO_FILL_COLOR); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - m_perimeter.render(); - m_fill.render(); - - glsafe(::glDisable(GL_BLEND)); - glsafe(::glEnable(GL_CULL_FACE)); - glsafe(::glDisable(GL_DEPTH_TEST)); - - shader->stop_using(); -} - -wxDEFINE_EVENT(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_OBJECT_SELECT, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_RIGHT_CLICK, RBtnEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_REMOVE_OBJECT, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_ARRANGE, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event); -wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent); -#if ENABLE_WORLD_COORDINATE -wxDEFINE_EVENT(EVT_GLCANVAS_RESET_SKEW, SimpleEvent); -#endif // ENABLE_WORLD_COORDINATE -wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3dEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, Event); -wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_GEOMETRY, Vec3dsEvent<2>); -wxDEFINE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_MOVE_SLIDERS, wxKeyEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_EDIT_COLOR_CHANGE, wxKeyEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_JUMP_TO, wxKeyEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_COLLAPSE_SIDEBAR, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, Event); -wxDEFINE_EVENT(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, HeightProfileSmoothEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_RELOAD_FROM_DISK, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_RENDER_TIMER, wxTimerEvent/*RenderTimerEvent*/); -wxDEFINE_EVENT(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, wxTimerEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, wxTimerEvent); - -const double GLCanvas3D::DefaultCameraZoomToBoxMarginFactor = 1.25; - -void GLCanvas3D::load_arrange_settings() -{ - std::string dist_fff_str = - wxGetApp().app_config->get("arrange", "min_object_distance_fff"); - - std::string dist_bed_fff_str = - wxGetApp().app_config->get("arrange", "min_bed_distance_fff"); - - std::string dist_fff_seq_print_str = - wxGetApp().app_config->get("arrange", "min_object_distance_fff_seq_print"); - - std::string dist_bed_fff_seq_print_str = - wxGetApp().app_config->get("arrange", "min_bed_distance_fff_seq_print"); - - std::string dist_sla_str = - wxGetApp().app_config->get("arrange", "min_object_distance_sla"); - - std::string dist_bed_sla_str = - wxGetApp().app_config->get("arrange", "min_bed_distance_sla"); - - std::string en_rot_fff_str = - wxGetApp().app_config->get("arrange", "enable_rotation_fff"); - - std::string en_rot_fff_seqp_str = - wxGetApp().app_config->get("arrange", "enable_rotation_fff_seq_print"); - - std::string en_rot_sla_str = - wxGetApp().app_config->get("arrange", "enable_rotation_sla"); - - if (!dist_fff_str.empty()) - m_arrange_settings_fff.distance = string_to_float_decimal_point(dist_fff_str); - - if (!dist_bed_fff_str.empty()) - m_arrange_settings_fff.distance_from_bed = string_to_float_decimal_point(dist_bed_fff_str); - - if (!dist_fff_seq_print_str.empty()) - m_arrange_settings_fff_seq_print.distance = string_to_float_decimal_point(dist_fff_seq_print_str); - - if (!dist_bed_fff_seq_print_str.empty()) - m_arrange_settings_fff_seq_print.distance_from_bed = string_to_float_decimal_point(dist_bed_fff_seq_print_str); - - if (!dist_sla_str.empty()) - m_arrange_settings_sla.distance = string_to_float_decimal_point(dist_sla_str); - - if (!dist_bed_sla_str.empty()) - m_arrange_settings_sla.distance_from_bed = string_to_float_decimal_point(dist_bed_sla_str); - - if (!en_rot_fff_str.empty()) - m_arrange_settings_fff.enable_rotation = (en_rot_fff_str == "1" || en_rot_fff_str == "yes"); - - if (!en_rot_fff_seqp_str.empty()) - m_arrange_settings_fff_seq_print.enable_rotation = (en_rot_fff_seqp_str == "1" || en_rot_fff_seqp_str == "yes"); - - if (!en_rot_sla_str.empty()) - m_arrange_settings_sla.enable_rotation = (en_rot_sla_str == "1" || en_rot_sla_str == "yes"); -} - -PrinterTechnology GLCanvas3D::current_printer_technology() const -{ - return m_process->current_printer_technology(); -} - -GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed) - : m_canvas(canvas) - , m_context(nullptr) - , m_bed(bed) -#if ENABLE_RETINA_GL - , m_retina_helper(nullptr) -#endif - , m_in_render(false) - , m_main_toolbar(GLToolbar::Normal, "Main") - , m_undoredo_toolbar(GLToolbar::Normal, "Undo_Redo") - , m_gizmos(*this) - , m_use_clipping_planes(false) - , m_sidebar_field("") - , m_extra_frame_requested(false) - , m_config(nullptr) - , m_process(nullptr) - , m_model(nullptr) - , m_dirty(true) - , m_initialized(false) - , m_apply_zoom_to_volumes_filter(false) - , m_picking_enabled(false) - , m_moving_enabled(false) - , m_dynamic_background_enabled(false) - , m_multisample_allowed(false) - , m_moving(false) - , m_tab_down(false) - , m_cursor_type(Standard) - , m_reload_delayed(false) - , m_render_sla_auxiliaries(true) - , m_labels(*this) - , m_slope(m_volumes) -{ - if (m_canvas != nullptr) { - m_timer.SetOwner(m_canvas); - m_render_timer.SetOwner(m_canvas); -#if ENABLE_RETINA_GL - m_retina_helper.reset(new RetinaHelper(canvas)); -#endif // ENABLE_RETINA_GL - } - - load_arrange_settings(); - - m_selection.set_volumes(&m_volumes.volumes); -} - -GLCanvas3D::~GLCanvas3D() -{ - reset_volumes(); -} - -void GLCanvas3D::post_event(wxEvent &&event) -{ - event.SetEventObject(m_canvas); - wxPostEvent(m_canvas, event); -} - -bool GLCanvas3D::init() -{ - if (m_initialized) - return true; - - if (m_canvas == nullptr || m_context == nullptr) - return false; - - glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 1.0f)); -#if ENABLE_OPENGL_ES - glsafe(::glClearDepthf(1.0f)); -#else - glsafe(::glClearDepth(1.0f)); -#endif // ENABLE_OPENGL_ES - - glsafe(::glDepthFunc(GL_LESS)); - - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glEnable(GL_CULL_FACE)); - glsafe(::glEnable(GL_BLEND)); - glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - // Set antialiasing / multisampling - glsafe(::glDisable(GL_LINE_SMOOTH)); - glsafe(::glDisable(GL_POLYGON_SMOOTH)); - - // ambient lighting - GLfloat ambient[4] = { 0.3f, 0.3f, 0.3f, 1.0f }; - glsafe(::glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient)); - - glsafe(::glEnable(GL_LIGHT0)); - glsafe(::glEnable(GL_LIGHT1)); - - // light from camera - GLfloat specular_cam[4] = { 0.3f, 0.3f, 0.3f, 1.0f }; - glsafe(::glLightfv(GL_LIGHT1, GL_SPECULAR, specular_cam)); - GLfloat diffuse_cam[4] = { 0.2f, 0.2f, 0.2f, 1.0f }; - glsafe(::glLightfv(GL_LIGHT1, GL_DIFFUSE, diffuse_cam)); - - // light from above - GLfloat specular_top[4] = { 0.2f, 0.2f, 0.2f, 1.0f }; - glsafe(::glLightfv(GL_LIGHT0, GL_SPECULAR, specular_top)); - GLfloat diffuse_top[4] = { 0.5f, 0.5f, 0.5f, 1.0f }; - glsafe(::glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse_top)); - - // Enables Smooth Color Shading; try GL_FLAT for (lack of) fun. - glsafe(::glShadeModel(GL_SMOOTH)); - - // A handy trick -- have surface material mirror the color. - glsafe(::glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)); - glsafe(::glEnable(GL_COLOR_MATERIAL)); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - if (m_multisample_allowed) - glsafe(::glEnable(GL_MULTISAMPLE)); - - if (m_main_toolbar.is_enabled()) - m_layers_editing.init(); - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - // on linux the gl context is not valid until the canvas is not shown on screen - // we defer the geometry finalization of volumes until the first call to render() - m_volumes.finalize_geometry(true); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - if (m_gizmos.is_enabled() && !m_gizmos.init()) - std::cout << "Unable to initialize gizmos: please, check that all the required textures are available" << std::endl; - - if (!_init_toolbars()) - return false; - - if (m_selection.is_enabled() && !m_selection.init()) - return false; - - m_initialized = true; - - return true; -} - -void GLCanvas3D::set_as_dirty() -{ - m_dirty = true; -} - -unsigned int GLCanvas3D::get_volumes_count() const -{ - return (unsigned int)m_volumes.volumes.size(); -} - -void GLCanvas3D::reset_volumes() -{ - if (!m_initialized) - return; - - if (m_volumes.empty()) - return; - - _set_current(); - - m_selection.clear(); - m_volumes.clear(); - m_dirty = true; - - _set_warning_notification(EWarning::ObjectOutside, false); -} - -ModelInstanceEPrintVolumeState GLCanvas3D::check_volumes_outside_state() const -{ - ModelInstanceEPrintVolumeState state = ModelInstanceEPrintVolumeState::ModelInstancePVS_Inside; - if (m_initialized) - m_volumes.check_outside_state(m_bed.build_volume(), &state); - return state; -} - -void GLCanvas3D::toggle_sla_auxiliaries_visibility(bool visible, const ModelObject* mo, int instance_idx) -{ - if (current_printer_technology() != ptSLA) - return; - - m_render_sla_auxiliaries = visible; - - for (GLVolume* vol : m_volumes.volumes) { - if ((mo == nullptr || m_model->objects[vol->composite_id.object_id] == mo) - && (instance_idx == -1 || vol->composite_id.instance_id == instance_idx) - && vol->composite_id.volume_id < 0) - vol->is_active = visible; - } -} - -void GLCanvas3D::toggle_model_objects_visibility(bool visible, const ModelObject* mo, int instance_idx, const ModelVolume* mv) -{ -#if ENABLE_RAYCAST_PICKING - std::vector>* raycasters = get_raycasters_for_picking(SceneRaycaster::EType::Volume); -#endif // ENABLE_RAYCAST_PICKING - for (GLVolume* vol : m_volumes.volumes) { - if (vol->is_wipe_tower) - vol->is_active = (visible && mo == nullptr); - else { - if ((mo == nullptr || m_model->objects[vol->composite_id.object_id] == mo) - && (instance_idx == -1 || vol->composite_id.instance_id == instance_idx) - && (mv == nullptr || m_model->objects[vol->composite_id.object_id]->volumes[vol->composite_id.volume_id] == mv)) { - vol->is_active = visible; - - if (instance_idx == -1) { - vol->force_native_color = false; - vol->force_neutral_color = false; - } else { - const GLGizmosManager& gm = get_gizmos_manager(); - auto gizmo_type = gm.get_current_type(); - if ( (gizmo_type == GLGizmosManager::FdmSupports - || gizmo_type == GLGizmosManager::Seam) - && ! vol->is_modifier) - vol->force_neutral_color = true; - else if (gizmo_type == GLGizmosManager::MmuSegmentation) - vol->is_active = false; - else - vol->force_native_color = true; - } - } - } -#if ENABLE_RAYCAST_PICKING - auto it = std::find_if(raycasters->begin(), raycasters->end(), [vol](std::shared_ptr item) { return item->get_raycaster() == vol->mesh_raycaster.get(); }); - if (it != raycasters->end()) - (*it)->set_active(vol->is_active); -#endif // ENABLE_RAYCAST_PICKING - } - - if (visible && !mo) - toggle_sla_auxiliaries_visibility(true, mo, instance_idx); - - if (!mo && !visible && !m_model->objects.empty() && (m_model->objects.size() > 1 || m_model->objects.front()->instances.size() > 1)) - _set_warning_notification(EWarning::SomethingNotShown, true); - - if (!mo && visible) - _set_warning_notification(EWarning::SomethingNotShown, false); -} - -void GLCanvas3D::update_instance_printable_state_for_object(const size_t obj_idx) -{ - ModelObject* model_object = m_model->objects[obj_idx]; - for (int inst_idx = 0; inst_idx < (int)model_object->instances.size(); ++inst_idx) { - ModelInstance* instance = model_object->instances[inst_idx]; - - for (GLVolume* volume : m_volumes.volumes) { - if (volume->object_idx() == (int)obj_idx && volume->instance_idx() == inst_idx) - volume->printable = instance->printable; - } - } -} - -void GLCanvas3D::update_instance_printable_state_for_objects(const std::vector& object_idxs) -{ - for (size_t obj_idx : object_idxs) - update_instance_printable_state_for_object(obj_idx); -} - -void GLCanvas3D::set_config(const DynamicPrintConfig* config) -{ - m_config = config; - m_layers_editing.set_config(config); -} - -void GLCanvas3D::set_process(BackgroundSlicingProcess *process) -{ - m_process = process; -} - -void GLCanvas3D::set_model(Model* model) -{ - m_model = model; - m_selection.set_model(m_model); -} - -void GLCanvas3D::bed_shape_changed() -{ - refresh_camera_scene_box(); - wxGetApp().plater()->get_camera().requires_zoom_to_bed = true; - m_dirty = true; -} - -void GLCanvas3D::refresh_camera_scene_box() -{ - wxGetApp().plater()->get_camera().set_scene_box(scene_bounding_box()); -} - -BoundingBoxf3 GLCanvas3D::volumes_bounding_box() const -{ - BoundingBoxf3 bb; - for (const GLVolume* volume : m_volumes.volumes) { - if (!m_apply_zoom_to_volumes_filter || ((volume != nullptr) && volume->zoom_to_volumes)) - bb.merge(volume->transformed_bounding_box()); - } - return bb; -} - -BoundingBoxf3 GLCanvas3D::scene_bounding_box() const -{ - BoundingBoxf3 bb = volumes_bounding_box(); - bb.merge(m_bed.extended_bounding_box()); - double h = m_bed.build_volume().max_print_height(); - //FIXME why -h? - bb.min.z() = std::min(bb.min.z(), -h); - bb.max.z() = std::max(bb.max.z(), h); - return bb; -} - -bool GLCanvas3D::is_layers_editing_enabled() const -{ - return m_layers_editing.is_enabled(); -} - -bool GLCanvas3D::is_layers_editing_allowed() const -{ - return m_layers_editing.is_allowed(); -} - -void GLCanvas3D::reset_layer_height_profile() -{ - wxGetApp().plater()->take_snapshot(_L("Variable layer height - Reset")); - m_layers_editing.reset_layer_height_profile(*this); - m_layers_editing.state = LayersEditing::Completed; - m_dirty = true; -} - -void GLCanvas3D::adaptive_layer_height_profile(float quality_factor) -{ - wxGetApp().plater()->take_snapshot(_L("Variable layer height - Adaptive")); - m_layers_editing.adaptive_layer_height_profile(*this, quality_factor); - m_layers_editing.state = LayersEditing::Completed; - m_dirty = true; -} - -void GLCanvas3D::smooth_layer_height_profile(const HeightProfileSmoothingParams& smoothing_params) -{ - wxGetApp().plater()->take_snapshot(_L("Variable layer height - Smooth all")); - m_layers_editing.smooth_layer_height_profile(*this, smoothing_params); - m_layers_editing.state = LayersEditing::Completed; - m_dirty = true; -} - -bool GLCanvas3D::is_reload_delayed() const -{ - return m_reload_delayed; -} - -void GLCanvas3D::enable_layers_editing(bool enable) -{ - m_layers_editing.set_enabled(enable); - set_as_dirty(); -} - -void GLCanvas3D::enable_legend_texture(bool enable) -{ - m_gcode_viewer.enable_legend(enable); -} - -void GLCanvas3D::enable_picking(bool enable) -{ - m_picking_enabled = enable; - m_selection.set_mode(Selection::Instance); -} - -void GLCanvas3D::enable_moving(bool enable) -{ - m_moving_enabled = enable; -} - -void GLCanvas3D::enable_gizmos(bool enable) -{ - m_gizmos.set_enabled(enable); -} - -void GLCanvas3D::enable_selection(bool enable) -{ - m_selection.set_enabled(enable); -} - -void GLCanvas3D::enable_main_toolbar(bool enable) -{ - m_main_toolbar.set_enabled(enable); -} - -void GLCanvas3D::enable_undoredo_toolbar(bool enable) -{ - m_undoredo_toolbar.set_enabled(enable); -} - -void GLCanvas3D::enable_dynamic_background(bool enable) -{ - m_dynamic_background_enabled = enable; -} - -void GLCanvas3D::allow_multisample(bool allow) -{ - m_multisample_allowed = allow; -} - -void GLCanvas3D::zoom_to_bed() -{ - BoundingBoxf3 box = m_bed.build_volume().bounding_volume(); - box.min.z() = 0.0; - box.max.z() = 0.0; - _zoom_to_box(box); -} - -void GLCanvas3D::zoom_to_volumes() -{ - m_apply_zoom_to_volumes_filter = true; - _zoom_to_box(volumes_bounding_box()); - m_apply_zoom_to_volumes_filter = false; -} - -void GLCanvas3D::zoom_to_selection() -{ - if (!m_selection.is_empty()) - _zoom_to_box(m_selection.get_bounding_box()); -} - -void GLCanvas3D::zoom_to_gcode() -{ - _zoom_to_box(m_gcode_viewer.get_paths_bounding_box(), 1.05); -} - -void GLCanvas3D::select_view(const std::string& direction) -{ - wxGetApp().plater()->get_camera().select_view(direction); - if (m_canvas != nullptr) - m_canvas->Refresh(); -} - -void GLCanvas3D::update_volumes_colors_by_extruder() -{ - if (m_config != nullptr) - m_volumes.update_colors_by_extruder(m_config); -} - -void GLCanvas3D::render() -{ - if (m_in_render) { - // if called recursively, return - m_dirty = true; - return; - } - - m_in_render = true; - Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; }); - (void)in_render_guard; - - if (m_canvas == nullptr) - return; - - // ensures this canvas is current and initialized - if (!_is_shown_on_screen() || !_set_current() || !wxGetApp().init_opengl()) - return; - - if (!is_initialized() && !init()) - return; - - if (!m_main_toolbar.is_enabled()) - m_gcode_viewer.init(); - - if (! m_bed.build_volume().valid()) { - // this happens at startup when no data is still saved under <>\AppData\Roaming\Slic3rPE - post_event(SimpleEvent(EVT_GLCANVAS_UPDATE_BED_SHAPE)); - return; - } - -#if ENABLE_ENVIRONMENT_MAP - if (wxGetApp().is_editor()) - wxGetApp().plater()->init_environment_texture(); -#endif // ENABLE_ENVIRONMENT_MAP - -#if ENABLE_GLMODEL_STATISTICS - GLModel::reset_statistics_counters(); -#endif // ENABLE_GLMODEL_STATISTICS - - const Size& cnv_size = get_canvas_size(); - // Probably due to different order of events on Linux/GTK2, when one switched from 3D scene - // to preview, this was called before canvas had its final size. It reported zero width - // and the viewport was set incorrectly, leading to tripping glAsserts further down - // the road (in apply_projection). That's why the minimum size is forced to 10. - Camera& camera = wxGetApp().plater()->get_camera(); -#if ENABLE_RAYCAST_PICKING - camera.set_viewport(0, 0, std::max(10u, (unsigned int)cnv_size.get_width()), std::max(10u, (unsigned int)cnv_size.get_height())); - camera.apply_viewport(); -#else - camera.apply_viewport(0, 0, std::max(10u, (unsigned int)cnv_size.get_width()), std::max(10u, (unsigned int)cnv_size.get_height())); -#endif // ENABLE_RAYCAST_PICKING - - if (camera.requires_zoom_to_bed) { - zoom_to_bed(); - _resize((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height()); - camera.requires_zoom_to_bed = false; - } - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - camera.apply_view_matrix(); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - camera.apply_projection(_max_bounding_box(true, true)); - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - GLfloat position_cam[4] = { 1.0f, 0.0f, 1.0f, 0.0f }; - glsafe(::glLightfv(GL_LIGHT1, GL_POSITION, position_cam)); - GLfloat position_top[4] = { -0.5f, -0.5f, 1.0f, 0.0f }; - glsafe(::glLightfv(GL_LIGHT0, GL_POSITION, position_top)); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - wxGetApp().imgui()->new_frame(); - - if (m_picking_enabled) { - if (m_rectangle_selection.is_dragging() && !m_rectangle_selection.is_empty()) - // picking pass using rectangle selection - _rectangular_selection_picking_pass(); - else if (!m_volumes.empty()) - // regular picking pass - _picking_pass(); -#if ENABLE_RAYCAST_PICKING_DEBUG - else { - ImGuiWrapper& imgui = *wxGetApp().imgui(); - imgui.begin(std::string("Hit result"), ImGuiWindowFlags_AlwaysAutoResize); - imgui.text("Picking disabled"); - imgui.end(); - } -#endif // ENABLE_RAYCAST_PICKING_DEBUG - } - - const bool is_looking_downward = camera.is_looking_downward(); - - // draw scene - glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); - _render_background(); - - _render_objects(GLVolumeCollection::ERenderType::Opaque); - if (!m_main_toolbar.is_enabled()) - _render_gcode(); - _render_sla_slices(); - _render_selection(); - if (is_looking_downward) -#if ENABLE_LEGACY_OPENGL_REMOVAL - _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), false, true); -#else - _render_bed(false, true); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - _render_objects(GLVolumeCollection::ERenderType::Transparent); - - _render_sequential_clearance(); -#if ENABLE_RENDER_SELECTION_CENTER - _render_selection_center(); -#endif // ENABLE_RENDER_SELECTION_CENTER - if (!m_main_toolbar.is_enabled()) - _render_gcode_cog(); - - // we need to set the mouse's scene position here because the depth buffer - // could be invalidated by the following gizmo render methods - // this position is used later into on_mouse() to drag the objects - if (m_picking_enabled) - m_mouse.scene_position = _mouse_to_3d(m_mouse.position.cast()); - - // sidebar hints need to be rendered before the gizmos because the depth buffer - // could be invalidated by the following gizmo render methods - _render_selection_sidebar_hints(); - _render_current_gizmo(); - if (!is_looking_downward) -#if ENABLE_LEGACY_OPENGL_REMOVAL - _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), true, true); -#else - _render_bed(true, true); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_RAYCAST_PICKING_DEBUG - if (m_picking_enabled && !m_mouse.dragging && !m_gizmos.is_dragging() && !m_rectangle_selection.is_dragging()) - m_scene_raycaster.render_hit(camera); -#endif // ENABLE_RAYCAST_PICKING_DEBUG - -#if ENABLE_SHOW_CAMERA_TARGET - _render_camera_target(); -#endif // ENABLE_SHOW_CAMERA_TARGET - - if (m_picking_enabled && m_rectangle_selection.is_dragging()) - m_rectangle_selection.render(*this); - - // draw overlays - _render_overlays(); - - if (wxGetApp().plater()->is_render_statistic_dialog_visible()) { - ImGuiWrapper& imgui = *wxGetApp().imgui(); - imgui.begin(std::string("Render statistics"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - imgui.text("FPS (SwapBuffers() calls per second):"); - ImGui::SameLine(); - imgui.text(std::to_string(m_render_stats.get_fps_and_reset_if_needed())); - ImGui::Separator(); - imgui.text("Compressed textures:"); - ImGui::SameLine(); - imgui.text(OpenGLManager::are_compressed_textures_supported() ? "supported" : "not supported"); - imgui.text("Max texture size:"); - ImGui::SameLine(); - imgui.text(std::to_string(OpenGLManager::get_gl_info().get_max_tex_size())); - imgui.end(); - } - -#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW - if (wxGetApp().is_editor() && wxGetApp().plater()->is_view3D_shown()) - wxGetApp().plater()->render_project_state_debug_window(); -#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW - -#if ENABLE_CAMERA_STATISTICS - camera.debug_render(); -#endif // ENABLE_CAMERA_STATISTICS -#if ENABLE_GLMODEL_STATISTICS - GLModel::render_statistics(); -#endif // ENABLE_GLMODEL_STATISTICS - - std::string tooltip; - - // Negative coordinate means out of the window, likely because the window was deactivated. - // In that case the tooltip should be hidden. - if (m_mouse.position.x() >= 0. && m_mouse.position.y() >= 0.) { - if (tooltip.empty()) - tooltip = m_layers_editing.get_tooltip(*this); - - if (tooltip.empty()) - tooltip = m_gizmos.get_tooltip(); - - if (tooltip.empty()) - tooltip = m_main_toolbar.get_tooltip(); - - if (tooltip.empty()) - tooltip = m_undoredo_toolbar.get_tooltip(); - - if (tooltip.empty()) - tooltip = wxGetApp().plater()->get_collapse_toolbar().get_tooltip(); - - if (tooltip.empty()) - tooltip = wxGetApp().plater()->get_view_toolbar().get_tooltip(); - } - - set_tooltip(tooltip); - - if (m_tooltip_enabled) - m_tooltip.render(m_mouse.position, *this); - - wxGetApp().plater()->get_mouse3d_controller().render_settings_dialog(*this); - - wxGetApp().plater()->get_notification_manager()->render_notifications(*this, get_overlay_window_width()); - - wxGetApp().imgui()->render(); - - m_canvas->SwapBuffers(); - m_render_stats.increment_fps_counter(); -} - -void GLCanvas3D::render_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, Camera::EType camera_type) -{ - render_thumbnail(thumbnail_data, w, h, thumbnail_params, m_volumes, camera_type); -} - -void GLCanvas3D::render_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type) -{ - switch (OpenGLManager::get_framebuffers_type()) - { - case OpenGLManager::EFramebufferType::Arb: { _render_thumbnail_framebuffer(thumbnail_data, w, h, thumbnail_params, volumes, camera_type); break; } - case OpenGLManager::EFramebufferType::Ext: { _render_thumbnail_framebuffer_ext(thumbnail_data, w, h, thumbnail_params, volumes, camera_type); break; } - default: { _render_thumbnail_legacy(thumbnail_data, w, h, thumbnail_params, volumes, camera_type); break; } - } -} - -void GLCanvas3D::select_all() -{ - m_selection.add_all(); - m_dirty = true; - wxGetApp().obj_manipul()->set_dirty(); - m_gizmos.reset_all_states(); - m_gizmos.update_data(); - post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); -} - -void GLCanvas3D::deselect_all() -{ - if (m_selection.is_empty()) - return; - - m_selection.remove_all(); - wxGetApp().obj_manipul()->set_dirty(); - m_gizmos.reset_all_states(); - m_gizmos.update_data(); - post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); -} - -void GLCanvas3D::delete_selected() -{ - m_selection.erase(); -} - -void GLCanvas3D::ensure_on_bed(unsigned int object_idx, bool allow_negative_z) -{ - if (allow_negative_z) - return; - - typedef std::map, double> InstancesToZMap; - InstancesToZMap instances_min_z; - - for (GLVolume* volume : m_volumes.volumes) { - if (volume->object_idx() == (int)object_idx && !volume->is_modifier) { - double min_z = volume->transformed_convex_hull_bounding_box().min.z(); - std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); - InstancesToZMap::iterator it = instances_min_z.find(instance); - if (it == instances_min_z.end()) - it = instances_min_z.insert(InstancesToZMap::value_type(instance, DBL_MAX)).first; - - it->second = std::min(it->second, min_z); - } - } - - for (GLVolume* volume : m_volumes.volumes) { - std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); - InstancesToZMap::iterator it = instances_min_z.find(instance); - if (it != instances_min_z.end()) - volume->set_instance_offset(Z, volume->get_instance_offset(Z) - it->second); - } -} - - -const std::vector& GLCanvas3D::get_gcode_layers_zs() const -{ - return m_gcode_viewer.get_layers_zs(); -} - -std::vector GLCanvas3D::get_volumes_print_zs(bool active_only) const -{ - return m_volumes.get_current_print_zs(active_only); -} - -void GLCanvas3D::set_gcode_options_visibility_from_flags(unsigned int flags) -{ - m_gcode_viewer.set_options_visibility_from_flags(flags); -} - -void GLCanvas3D::set_toolpath_role_visibility_flags(unsigned int flags) -{ - m_gcode_viewer.set_toolpath_role_visibility_flags(flags); -} - -void GLCanvas3D::set_toolpath_view_type(GCodeViewer::EViewType type) -{ - m_gcode_viewer.set_view_type(type); -} - -void GLCanvas3D::set_volumes_z_range(const std::array& range) -{ - m_volumes.set_range(range[0] - 1e-6, range[1] + 1e-6); -} - -void GLCanvas3D::set_toolpaths_z_range(const std::array& range) -{ - if (m_gcode_viewer.has_data()) - m_gcode_viewer.set_layers_z_range(range); -} - -std::vector GLCanvas3D::load_object(const ModelObject& model_object, int obj_idx, std::vector instance_idxs) -{ - if (instance_idxs.empty()) { - for (unsigned int i = 0; i < model_object.instances.size(); ++i) { - instance_idxs.emplace_back(i); - } - } -#if ENABLE_LEGACY_OPENGL_REMOVAL - return m_volumes.load_object(&model_object, obj_idx, instance_idxs); -#else - return m_volumes.load_object(&model_object, obj_idx, instance_idxs, m_initialized); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -std::vector GLCanvas3D::load_object(const Model& model, int obj_idx) -{ - if (0 <= obj_idx && obj_idx < (int)model.objects.size()) { - const ModelObject* model_object = model.objects[obj_idx]; - if (model_object != nullptr) - return load_object(*model_object, obj_idx, std::vector()); - } - - return std::vector(); -} - -void GLCanvas3D::mirror_selection(Axis axis) -{ - m_selection.mirror(axis); - do_mirror(L("Mirror Object")); - wxGetApp().obj_manipul()->set_dirty(); -} - -// Reload the 3D scene of -// 1) Model / ModelObjects / ModelInstances / ModelVolumes -// 2) Print bed -// 3) SLA support meshes for their respective ModelObjects / ModelInstances -// 4) Wipe tower preview -// 5) Out of bed collision status & message overlay (texture) -void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_refresh) -{ - if (m_canvas == nullptr || m_config == nullptr || m_model == nullptr) - return; - - if (!m_initialized) - return; - - _set_current(); - - m_hover_volume_idxs.clear(); - - struct ModelVolumeState { - ModelVolumeState(const GLVolume* volume) : - model_volume(nullptr), geometry_id(volume->geometry_id), volume_idx(-1) {} - ModelVolumeState(const ModelVolume* model_volume, const ObjectID& instance_id, const GLVolume::CompositeID& composite_id) : - model_volume(model_volume), geometry_id(std::make_pair(model_volume->id().id, instance_id.id)), composite_id(composite_id), volume_idx(-1) {} - ModelVolumeState(const ObjectID& volume_id, const ObjectID& instance_id) : - model_volume(nullptr), geometry_id(std::make_pair(volume_id.id, instance_id.id)), volume_idx(-1) {} - bool new_geometry() const { return this->volume_idx == size_t(-1); } - const ModelVolume* model_volume; - // ObjectID of ModelVolume + ObjectID of ModelInstance - // or timestamp of an SLAPrintObjectStep + ObjectID of ModelInstance - std::pair geometry_id; - GLVolume::CompositeID composite_id; - // Volume index in the new GLVolume vector. - size_t volume_idx; - }; - std::vector model_volume_state; - std::vector aux_volume_state; - - struct GLVolumeState { - GLVolumeState() : - volume_idx(size_t(-1)) {} - GLVolumeState(const GLVolume* volume, unsigned int volume_idx) : - composite_id(volume->composite_id), volume_idx(volume_idx) {} - GLVolumeState(const GLVolume::CompositeID &composite_id) : - composite_id(composite_id), volume_idx(size_t(-1)) {} - - GLVolume::CompositeID composite_id; - // Volume index in the old GLVolume vector. - size_t volume_idx; - }; - - // SLA steps to pull the preview meshes for. - typedef std::array SLASteps; - SLASteps sla_steps = { slaposDrillHoles, slaposSupportTree, slaposPad }; - struct SLASupportState { - std::array::value> step; - }; - // State of the sla_steps for all SLAPrintObjects. - std::vector sla_support_state; - - std::vector instance_ids_selected; - std::vector map_glvolume_old_to_new(m_volumes.volumes.size(), size_t(-1)); - std::vector deleted_volumes; - std::vector glvolumes_new; - glvolumes_new.reserve(m_volumes.volumes.size()); - auto model_volume_state_lower = [](const ModelVolumeState& m1, const ModelVolumeState& m2) { return m1.geometry_id < m2.geometry_id; }; - - m_reload_delayed = !m_canvas->IsShown() && !refresh_immediately && !force_full_scene_refresh; - - PrinterTechnology printer_technology = current_printer_technology(); - int volume_idx_wipe_tower_old = -1; - - // Release invalidated volumes to conserve GPU memory in case of delayed refresh (see m_reload_delayed). - // First initialize model_volumes_new_sorted & model_instances_new_sorted. - for (int object_idx = 0; object_idx < (int)m_model->objects.size(); ++object_idx) { - const ModelObject* model_object = m_model->objects[object_idx]; - for (int instance_idx = 0; instance_idx < (int)model_object->instances.size(); ++instance_idx) { - const ModelInstance* model_instance = model_object->instances[instance_idx]; - for (int volume_idx = 0; volume_idx < (int)model_object->volumes.size(); ++volume_idx) { - const ModelVolume* model_volume = model_object->volumes[volume_idx]; - model_volume_state.emplace_back(model_volume, model_instance->id(), GLVolume::CompositeID(object_idx, volume_idx, instance_idx)); - } - } - } - if (printer_technology == ptSLA) { - const SLAPrint* sla_print = this->sla_print(); -#ifndef NDEBUG - // Verify that the SLAPrint object is synchronized with m_model. - check_model_ids_equal(*m_model, sla_print->model()); -#endif /* NDEBUG */ - sla_support_state.reserve(sla_print->objects().size()); - for (const SLAPrintObject* print_object : sla_print->objects()) { - SLASupportState state; - for (size_t istep = 0; istep < sla_steps.size(); ++istep) { - state.step[istep] = print_object->step_state_with_timestamp(sla_steps[istep]); - if (state.step[istep].state == PrintStateBase::DONE) { - if (!print_object->has_mesh(sla_steps[istep])) - // Consider the DONE step without a valid mesh as invalid for the purpose - // of mesh visualization. - state.step[istep].state = PrintStateBase::INVALID; - else if (sla_steps[istep] != slaposDrillHoles) - for (const ModelInstance* model_instance : print_object->model_object()->instances) - // Only the instances, which are currently printable, will have the SLA support structures kept. - // The instances outside the print bed will have the GLVolumes of their support structures released. - if (model_instance->is_printable()) - aux_volume_state.emplace_back(state.step[istep].timestamp, model_instance->id()); - } - } - sla_support_state.emplace_back(state); - } - } - std::sort(model_volume_state.begin(), model_volume_state.end(), model_volume_state_lower); - std::sort(aux_volume_state.begin(), aux_volume_state.end(), model_volume_state_lower); - // Release all ModelVolume based GLVolumes not found in the current Model. Find the GLVolume of a hollowed mesh. - for (size_t volume_id = 0; volume_id < m_volumes.volumes.size(); ++volume_id) { - GLVolume* volume = m_volumes.volumes[volume_id]; - ModelVolumeState key(volume); - ModelVolumeState* mvs = nullptr; - if (volume->volume_idx() < 0) { - auto it = std::lower_bound(aux_volume_state.begin(), aux_volume_state.end(), key, model_volume_state_lower); - if (it != aux_volume_state.end() && it->geometry_id == key.geometry_id) - // This can be an SLA support structure that should not be rendered (in case someone used undo - // to revert to before it was generated). We only reuse the volume if that's not the case. - if (m_model->objects[volume->composite_id.object_id]->sla_points_status != sla::PointsStatus::NoPoints) - mvs = &(*it); - } - else { - auto it = std::lower_bound(model_volume_state.begin(), model_volume_state.end(), key, model_volume_state_lower); - if (it != model_volume_state.end() && it->geometry_id == key.geometry_id) - mvs = &(*it); - } - // Emplace instance ID of the volume. Both the aux volumes and model volumes share the same instance ID. - // The wipe tower has its own wipe_tower_instance_id(). - if (m_selection.contains_volume(volume_id)) - instance_ids_selected.emplace_back(volume->geometry_id.second); - if (mvs == nullptr || force_full_scene_refresh) { - // This GLVolume will be released. - if (volume->is_wipe_tower) { - // There is only one wipe tower. - assert(volume_idx_wipe_tower_old == -1); -#if ENABLE_OPENGL_ES - m_wipe_tower_mesh.clear(); -#endif // ENABLE_OPENGL_ES - volume_idx_wipe_tower_old = (int)volume_id; - } - if (!m_reload_delayed) { - deleted_volumes.emplace_back(volume, volume_id); - delete volume; - } - } - else { - // This GLVolume will be reused. - volume->set_sla_shift_z(0.0); - map_glvolume_old_to_new[volume_id] = glvolumes_new.size(); - mvs->volume_idx = glvolumes_new.size(); - glvolumes_new.emplace_back(volume); - // Update color of the volume based on the current extruder. - if (mvs->model_volume != nullptr) { - int extruder_id = mvs->model_volume->extruder_id(); - if (extruder_id != -1) - volume->extruder_id = extruder_id; - - volume->is_modifier = !mvs->model_volume->is_model_part(); - volume->set_color(color_from_model_volume(*mvs->model_volume)); - // force update of render_color alpha channel - volume->set_render_color(volume->color.is_transparent()); - - // updates volumes transformations - volume->set_instance_transformation(mvs->model_volume->get_object()->instances[mvs->composite_id.instance_id]->get_transformation()); - volume->set_volume_transformation(mvs->model_volume->get_transformation()); - - // updates volumes convex hull - if (mvs->model_volume->is_model_part() && ! volume->convex_hull()) - // Model volume was likely changed from modifier or support blocker / enforcer to a model part. - // Only model parts require convex hulls. - volume->set_convex_hull(mvs->model_volume->get_convex_hull_shared_ptr()); - } - } - } - sort_remove_duplicates(instance_ids_selected); - auto deleted_volumes_lower = [](const GLVolumeState &v1, const GLVolumeState &v2) { return v1.composite_id < v2.composite_id; }; - std::sort(deleted_volumes.begin(), deleted_volumes.end(), deleted_volumes_lower); - - if (m_reload_delayed) - return; - - bool update_object_list = false; - if (m_volumes.volumes != glvolumes_new) - update_object_list = true; - m_volumes.volumes = std::move(glvolumes_new); - for (unsigned int obj_idx = 0; obj_idx < (unsigned int)m_model->objects.size(); ++ obj_idx) { - const ModelObject &model_object = *m_model->objects[obj_idx]; - for (int volume_idx = 0; volume_idx < (int)model_object.volumes.size(); ++ volume_idx) { - const ModelVolume &model_volume = *model_object.volumes[volume_idx]; - for (int instance_idx = 0; instance_idx < (int)model_object.instances.size(); ++ instance_idx) { - const ModelInstance &model_instance = *model_object.instances[instance_idx]; - ModelVolumeState key(model_volume.id(), model_instance.id()); - auto it = std::lower_bound(model_volume_state.begin(), model_volume_state.end(), key, model_volume_state_lower); - assert(it != model_volume_state.end() && it->geometry_id == key.geometry_id); - if (it->new_geometry()) { - // New volume. - auto it_old_volume = std::lower_bound(deleted_volumes.begin(), deleted_volumes.end(), GLVolumeState(it->composite_id), deleted_volumes_lower); - if (it_old_volume != deleted_volumes.end() && it_old_volume->composite_id == it->composite_id) - // If a volume changed its ObjectID, but it reuses a GLVolume's CompositeID, maintain its selection. - map_glvolume_old_to_new[it_old_volume->volume_idx] = m_volumes.volumes.size(); - // Note the index of the loaded volume, so that we can reload the main model GLVolume with the hollowed mesh - // later in this function. - it->volume_idx = m_volumes.volumes.size(); -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_volumes.load_object_volume(&model_object, obj_idx, volume_idx, instance_idx); -#else - m_volumes.load_object_volume(&model_object, obj_idx, volume_idx, instance_idx, m_initialized); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - m_volumes.volumes.back()->geometry_id = key.geometry_id; - update_object_list = true; - } else { - // Recycling an old GLVolume. - GLVolume &existing_volume = *m_volumes.volumes[it->volume_idx]; - assert(existing_volume.geometry_id == key.geometry_id); - // Update the Object/Volume/Instance indices into the current Model. - if (existing_volume.composite_id != it->composite_id) { - existing_volume.composite_id = it->composite_id; - update_object_list = true; - } - } - } - } - } - if (printer_technology == ptSLA) { - size_t idx = 0; - const SLAPrint *sla_print = this->sla_print(); - std::vector shift_zs(m_model->objects.size(), 0); - double relative_correction_z = sla_print->relative_correction().z(); - if (relative_correction_z <= EPSILON) - relative_correction_z = 1.; - for (const SLAPrintObject *print_object : sla_print->objects()) { - SLASupportState &state = sla_support_state[idx ++]; - const ModelObject *model_object = print_object->model_object(); - // Find an index of the ModelObject - int object_idx; - // There may be new SLA volumes added to the scene for this print_object. - // Find the object index of this print_object in the Model::objects list. - auto it = std::find(sla_print->model().objects.begin(), sla_print->model().objects.end(), model_object); - assert(it != sla_print->model().objects.end()); - object_idx = it - sla_print->model().objects.begin(); - // Cache the Z offset to be applied to all volumes with this object_idx. - shift_zs[object_idx] = print_object->get_current_elevation() / relative_correction_z; - // Collect indices of this print_object's instances, for which the SLA support meshes are to be added to the scene. - // pairs of - std::vector> instances[std::tuple_size::value]; - for (size_t print_instance_idx = 0; print_instance_idx < print_object->instances().size(); ++ print_instance_idx) { - const SLAPrintObject::Instance &instance = print_object->instances()[print_instance_idx]; - // Find index of ModelInstance corresponding to this SLAPrintObject::Instance. - auto it = std::find_if(model_object->instances.begin(), model_object->instances.end(), - [&instance](const ModelInstance *mi) { return mi->id() == instance.instance_id; }); - assert(it != model_object->instances.end()); - int instance_idx = it - model_object->instances.begin(); - for (size_t istep = 0; istep < sla_steps.size(); ++ istep) - if (sla_steps[istep] == slaposDrillHoles) { - // Hollowing is a special case, where the mesh from the backend is being loaded into the 1st volume of an instance, - // not into its own GLVolume. - // There shall always be such a GLVolume allocated. - ModelVolumeState key(model_object->volumes.front()->id(), instance.instance_id); - auto it = std::lower_bound(model_volume_state.begin(), model_volume_state.end(), key, model_volume_state_lower); - assert(it != model_volume_state.end() && it->geometry_id == key.geometry_id); - assert(!it->new_geometry()); - GLVolume &volume = *m_volumes.volumes[it->volume_idx]; - if (! volume.offsets.empty() && state.step[istep].timestamp != volume.offsets.front()) { - // The backend either produced a new hollowed mesh, or it invalidated the one that the front end has seen. -#if ENABLE_LEGACY_OPENGL_REMOVAL - volume.model.reset(); -#else - volume.indexed_vertex_array.release_geometry(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - if (state.step[istep].state == PrintStateBase::DONE) { - TriangleMesh mesh = print_object->get_mesh(slaposDrillHoles); - assert(! mesh.empty()); - - // sla_trafo does not contain volume trafo. To get a mesh to create - // a new volume from, we have to apply vol trafo inverse separately. - const ModelObject& mo = *m_model->objects[volume.object_idx()]; - Transform3d trafo = sla_print->sla_trafo(mo) - * mo.volumes.front()->get_transformation().get_matrix(); - mesh.transform(trafo.inverse()); -#if ENABLE_SMOOTH_NORMALS -#if ENABLE_LEGACY_OPENGL_REMOVAL - volume.model.init_from(mesh, true); -#else - volume.indexed_vertex_array.load_mesh(mesh, true); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#else -#if ENABLE_LEGACY_OPENGL_REMOVAL - volume.model.init_from(mesh); -#if ENABLE_RAYCAST_PICKING - volume.mesh_raycaster = std::make_unique(std::make_shared(mesh)); -#endif // ENABLE_RAYCAST_PICKING -#else - volume.indexed_vertex_array.load_mesh(mesh); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#endif // ENABLE_SMOOTH_NORMALS - } - else { - // Reload the original volume. -#if ENABLE_SMOOTH_NORMALS -#if ENABLE_LEGACY_OPENGL_REMOVAL - volume.model.init_from(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(), true); -#else - volume.indexed_vertex_array.load_mesh(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(), true); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#else -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_RAYCAST_PICKING - const TriangleMesh& new_mesh = m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(); - volume.model.init_from(new_mesh); - volume.mesh_raycaster = std::make_unique(std::make_shared(new_mesh)); -#else - volume.model.init_from(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh()); -#endif // ENABLE_RAYCAST_PICKING -#else - volume.indexed_vertex_array.load_mesh(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#endif // ENABLE_SMOOTH_NORMALS - } -#if !ENABLE_LEGACY_OPENGL_REMOVAL - volume.finalize_geometry(true); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - } - //FIXME it is an ugly hack to write the timestamp into the "offsets" field to not have to add another member variable - // to the GLVolume. We should refactor GLVolume significantly, so that the GLVolume will not contain member variables - // of various concenrs (model vs. 3D print path). - volume.offsets = { state.step[istep].timestamp }; - } - else if (state.step[istep].state == PrintStateBase::DONE) { - // Check whether there is an existing auxiliary volume to be updated, or a new auxiliary volume to be created. - ModelVolumeState key(state.step[istep].timestamp, instance.instance_id.id); - auto it = std::lower_bound(aux_volume_state.begin(), aux_volume_state.end(), key, model_volume_state_lower); - assert(it != aux_volume_state.end() && it->geometry_id == key.geometry_id); - if (it->new_geometry()) { - // This can be an SLA support structure that should not be rendered (in case someone used undo - // to revert to before it was generated). If that's the case, we should not generate anything. - if (model_object->sla_points_status != sla::PointsStatus::NoPoints) - instances[istep].emplace_back(std::pair(instance_idx, print_instance_idx)); - else - shift_zs[object_idx] = 0.; - } - else { - // Recycling an old GLVolume. Update the Object/Instance indices into the current Model. - m_volumes.volumes[it->volume_idx]->composite_id = GLVolume::CompositeID(object_idx, m_volumes.volumes[it->volume_idx]->volume_idx(), instance_idx); - m_volumes.volumes[it->volume_idx]->set_instance_transformation(model_object->instances[instance_idx]->get_transformation()); - } - } - } - - for (size_t istep = 0; istep < sla_steps.size(); ++istep) - if (!instances[istep].empty()) -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_volumes.load_object_auxiliary(print_object, object_idx, instances[istep], sla_steps[istep], state.step[istep].timestamp); -#else - m_volumes.load_object_auxiliary(print_object, object_idx, instances[istep], sla_steps[istep], state.step[istep].timestamp, m_initialized); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - - // Shift-up all volumes of the object so that it has the right elevation with respect to the print bed - for (GLVolume* volume : m_volumes.volumes) - if (volume->object_idx() < (int)m_model->objects.size() && m_model->objects[volume->object_idx()]->instances[volume->instance_idx()]->is_printable()) - volume->set_sla_shift_z(shift_zs[volume->object_idx()]); - } - - if (printer_technology == ptFFF && m_config->has("nozzle_diameter")) { - // Should the wipe tower be visualized ? - unsigned int extruders_count = (unsigned int)dynamic_cast(m_config->option("nozzle_diameter"))->values.size(); - - bool wt = dynamic_cast(m_config->option("wipe_tower"))->value; - bool co = dynamic_cast(m_config->option("complete_objects"))->value; - - if (extruders_count > 1 && wt && !co) { - // Height of a print (Show at least a slab) - double height = std::max(m_model->bounding_box().max(2), 10.0); - - float x = dynamic_cast(m_config->option("wipe_tower_x"))->value; - float y = dynamic_cast(m_config->option("wipe_tower_y"))->value; - float w = dynamic_cast(m_config->option("wipe_tower_width"))->value; - float a = dynamic_cast(m_config->option("wipe_tower_rotation_angle"))->value; - - const Print *print = m_process->fff_print(); - - float depth = print->wipe_tower_data(extruders_count).depth; - float brim_width = print->wipe_tower_data(extruders_count).brim_width; - -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_OPENGL_ES - int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview( - x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower), - brim_width, &m_wipe_tower_mesh); -#else - int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview( - x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower), - brim_width); -#endif // ENABLE_OPENGL_ES -#else - int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview( - x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower), - brim_width, m_initialized); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - if (volume_idx_wipe_tower_old != -1) - map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new; - } - } - - update_volumes_colors_by_extruder(); - // Update selection indices based on the old/new GLVolumeCollection. - if (m_selection.get_mode() == Selection::Instance) - m_selection.instances_changed(instance_ids_selected); - else - m_selection.volumes_changed(map_glvolume_old_to_new); - - m_gizmos.update_data(); - m_gizmos.refresh_on_off_state(); - - // Update the toolbar - if (update_object_list) - post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); - - // checks for geometry outside the print volume to render it accordingly - if (!m_volumes.empty()) { - ModelInstanceEPrintVolumeState state; - const bool contained_min_one = m_volumes.check_outside_state(m_bed.build_volume(), &state); - const bool partlyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Partly_Outside); - const bool fullyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Fully_Outside); - - if (printer_technology != ptSLA) { - _set_warning_notification(EWarning::ObjectClashed, partlyOut); - _set_warning_notification(EWarning::ObjectOutside, fullyOut); - _set_warning_notification(EWarning::SlaSupportsOutside, false); - } - else { - const auto [res, volume] = _is_any_volume_outside(); - const bool is_support = volume != nullptr && volume->is_sla_support(); - if (is_support) { - _set_warning_notification(EWarning::ObjectClashed, false); - _set_warning_notification(EWarning::ObjectOutside, false); - _set_warning_notification(EWarning::SlaSupportsOutside, partlyOut || fullyOut); - } - else { - _set_warning_notification(EWarning::ObjectClashed, partlyOut); - _set_warning_notification(EWarning::ObjectOutside, fullyOut); - _set_warning_notification(EWarning::SlaSupportsOutside, false); - } - } - - post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, - contained_min_one && !m_model->objects.empty() && !partlyOut)); - } - else { - _set_warning_notification(EWarning::ObjectOutside, false); - _set_warning_notification(EWarning::ObjectClashed, false); - _set_warning_notification(EWarning::SlaSupportsOutside, false); - post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); - } - - refresh_camera_scene_box(); - - if (m_selection.is_empty()) { - // If no object is selected, deactivate the active gizmo, if any - // Otherwise it may be shown after cleaning the scene (if it was active while the objects were deleted) - m_gizmos.reset_all_states(); - - // If no object is selected, reset the objects manipulator on the sidebar - // to force a reset of its cache - auto manip = wxGetApp().obj_manipul(); - if (manip != nullptr) - manip->set_dirty(); - } - -#if ENABLE_RAYCAST_PICKING - // refresh volume raycasters for picking - m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::Volume); - for (size_t i = 0; i < m_volumes.volumes.size(); ++i) { - assert(m_volumes.volumes[i]->mesh_raycaster != nullptr); - add_raycaster_for_picking(SceneRaycaster::EType::Volume, i, *m_volumes.volumes[i]->mesh_raycaster, m_volumes.volumes[i]->world_matrix()); - } - - // refresh gizmo elements raycasters for picking - GLGizmoBase* curr_gizmo = m_gizmos.get_current(); - if (curr_gizmo != nullptr) - curr_gizmo->unregister_raycasters_for_picking(); - m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::Gizmo); - if (curr_gizmo != nullptr && !m_selection.is_empty()) - curr_gizmo->register_raycasters_for_picking(); -#endif // ENABLE_RAYCAST_PICKING - - // and force this canvas to be redrawn. - m_dirty = true; -} - -#if !ENABLE_LEGACY_OPENGL_REMOVAL -static void reserve_new_volume_finalize_old_volume(GLVolume& vol_new, GLVolume& vol_old, bool gl_initialized, size_t prealloc_size = VERTEX_BUFFER_RESERVE_SIZE) -{ - // Assign the large pre-allocated buffers to the new GLVolume. - vol_new.indexed_vertex_array = std::move(vol_old.indexed_vertex_array); - // Copy the content back to the old GLVolume. - vol_old.indexed_vertex_array = vol_new.indexed_vertex_array; - // Clear the buffers, but keep them pre-allocated. - vol_new.indexed_vertex_array.clear(); - // Just make sure that clear did not clear the reserved memory. - // Reserving number of vertices (3x position + 3x color) - vol_new.indexed_vertex_array.reserve(prealloc_size / 6); - // Finalize the old geometry, possibly move data to the graphics card. - vol_old.finalize_geometry(gl_initialized); -} -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - -void GLCanvas3D::load_gcode_preview(const GCodeProcessorResult& gcode_result, const std::vector& str_tool_colors) -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_gcode_viewer.load(gcode_result, *this->fff_print()); -#else - m_gcode_viewer.load(gcode_result, *this->fff_print(), m_initialized); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - if (wxGetApp().is_editor()) { - m_gcode_viewer.update_shells_color_by_extruder(m_config); - _set_warning_notification_if_needed(EWarning::ToolpathOutside); - } - - m_gcode_viewer.refresh(gcode_result, str_tool_colors); - set_as_dirty(); - request_extra_frame(); -} - -void GLCanvas3D::refresh_gcode_preview_render_paths(bool keep_sequential_current_first, bool keep_sequential_current_last) -{ - m_gcode_viewer.refresh_render_paths(keep_sequential_current_first, keep_sequential_current_last); - set_as_dirty(); - request_extra_frame(); -} - -void GLCanvas3D::load_sla_preview() -{ - const SLAPrint* print = sla_print(); - if (m_canvas != nullptr && print != nullptr) { - _set_current(); - // Release OpenGL data before generating new data. - reset_volumes(); - _load_sla_shells(); - _update_sla_shells_outside_state(); - _set_warning_notification_if_needed(EWarning::ObjectClashed); - _set_warning_notification_if_needed(EWarning::SlaSupportsOutside); - } -} - -void GLCanvas3D::load_preview(const std::vector& str_tool_colors, const std::vector& color_print_values) -{ - const Print *print = this->fff_print(); - if (print == nullptr) - return; - - _set_current(); - - // Release OpenGL data before generating new data. - this->reset_volumes(); - - const BuildVolume &build_volume = m_bed.build_volume(); - _load_print_toolpaths(build_volume); - _load_wipe_tower_toolpaths(build_volume, str_tool_colors); - for (const PrintObject* object : print->objects()) - _load_print_object_toolpaths(*object, build_volume, str_tool_colors, color_print_values); - - _set_warning_notification_if_needed(EWarning::ToolpathOutside); -} - -void GLCanvas3D::bind_event_handlers() -{ - if (m_canvas != nullptr) { - m_canvas->Bind(wxEVT_SIZE, &GLCanvas3D::on_size, this); - m_canvas->Bind(wxEVT_IDLE, &GLCanvas3D::on_idle, this); - m_canvas->Bind(wxEVT_CHAR, &GLCanvas3D::on_char, this); - m_canvas->Bind(wxEVT_KEY_DOWN, &GLCanvas3D::on_key, this); - m_canvas->Bind(wxEVT_KEY_UP, &GLCanvas3D::on_key, this); - m_canvas->Bind(wxEVT_MOUSEWHEEL, &GLCanvas3D::on_mouse_wheel, this); - m_canvas->Bind(wxEVT_TIMER, &GLCanvas3D::on_timer, this); - m_canvas->Bind(EVT_GLCANVAS_RENDER_TIMER, &GLCanvas3D::on_render_timer, this); - m_toolbar_highlighter.set_timer_owner(m_canvas, 0); - m_canvas->Bind(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_toolbar_highlighter.blink(); }); - m_gizmo_highlighter.set_timer_owner(m_canvas, 0); - m_canvas->Bind(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_gizmo_highlighter.blink(); }); - m_canvas->Bind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this); - m_canvas->Bind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this); - m_canvas->Bind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this); - m_canvas->Bind(wxEVT_MIDDLE_UP, &GLCanvas3D::on_mouse, this); - m_canvas->Bind(wxEVT_RIGHT_DOWN, &GLCanvas3D::on_mouse, this); - m_canvas->Bind(wxEVT_RIGHT_UP, &GLCanvas3D::on_mouse, this); - m_canvas->Bind(wxEVT_MOTION, &GLCanvas3D::on_mouse, this); - m_canvas->Bind(wxEVT_ENTER_WINDOW, &GLCanvas3D::on_mouse, this); - m_canvas->Bind(wxEVT_LEAVE_WINDOW, &GLCanvas3D::on_mouse, this); - m_canvas->Bind(wxEVT_LEFT_DCLICK, &GLCanvas3D::on_mouse, this); - m_canvas->Bind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this); - m_canvas->Bind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this); - m_canvas->Bind(wxEVT_PAINT, &GLCanvas3D::on_paint, this); - m_canvas->Bind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this); - - m_event_handlers_bound = true; - } -} - -void GLCanvas3D::unbind_event_handlers() -{ - if (m_canvas != nullptr && m_event_handlers_bound) { - m_canvas->Unbind(wxEVT_SIZE, &GLCanvas3D::on_size, this); - m_canvas->Unbind(wxEVT_IDLE, &GLCanvas3D::on_idle, this); - m_canvas->Unbind(wxEVT_CHAR, &GLCanvas3D::on_char, this); - m_canvas->Unbind(wxEVT_KEY_DOWN, &GLCanvas3D::on_key, this); - m_canvas->Unbind(wxEVT_KEY_UP, &GLCanvas3D::on_key, this); - m_canvas->Unbind(wxEVT_MOUSEWHEEL, &GLCanvas3D::on_mouse_wheel, this); - m_canvas->Unbind(wxEVT_TIMER, &GLCanvas3D::on_timer, this); - m_canvas->Unbind(EVT_GLCANVAS_RENDER_TIMER, &GLCanvas3D::on_render_timer, this); - m_canvas->Unbind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this); - m_canvas->Unbind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this); - m_canvas->Unbind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this); - m_canvas->Unbind(wxEVT_MIDDLE_UP, &GLCanvas3D::on_mouse, this); - m_canvas->Unbind(wxEVT_RIGHT_DOWN, &GLCanvas3D::on_mouse, this); - m_canvas->Unbind(wxEVT_RIGHT_UP, &GLCanvas3D::on_mouse, this); - m_canvas->Unbind(wxEVT_MOTION, &GLCanvas3D::on_mouse, this); - m_canvas->Unbind(wxEVT_ENTER_WINDOW, &GLCanvas3D::on_mouse, this); - m_canvas->Unbind(wxEVT_LEAVE_WINDOW, &GLCanvas3D::on_mouse, this); - m_canvas->Unbind(wxEVT_LEFT_DCLICK, &GLCanvas3D::on_mouse, this); - m_canvas->Unbind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this); - m_canvas->Unbind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this); - m_canvas->Unbind(wxEVT_PAINT, &GLCanvas3D::on_paint, this); - m_canvas->Unbind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this); - - m_event_handlers_bound = false; - } -} - -void GLCanvas3D::on_size(wxSizeEvent& evt) -{ - m_dirty = true; -} - -void GLCanvas3D::on_idle(wxIdleEvent& evt) -{ - if (!m_initialized) - return; - - m_dirty |= m_main_toolbar.update_items_state(); - m_dirty |= m_undoredo_toolbar.update_items_state(); - m_dirty |= wxGetApp().plater()->get_view_toolbar().update_items_state(); - m_dirty |= wxGetApp().plater()->get_collapse_toolbar().update_items_state(); - bool mouse3d_controller_applied = wxGetApp().plater()->get_mouse3d_controller().apply(wxGetApp().plater()->get_camera()); - m_dirty |= mouse3d_controller_applied; - m_dirty |= wxGetApp().plater()->get_notification_manager()->update_notifications(*this); - auto gizmo = wxGetApp().plater()->canvas3D()->get_gizmos_manager().get_current(); - if (gizmo != nullptr) m_dirty |= gizmo->update_items_state(); - // ImGuiWrapper::m_requires_extra_frame may have been set by a render made outside of the OnIdle mechanism - bool imgui_requires_extra_frame = wxGetApp().imgui()->requires_extra_frame(); - m_dirty |= imgui_requires_extra_frame; - - if (!m_dirty) - return; - - // this needs to be done here. - // during the render launched by the refresh the value may be set again - wxGetApp().imgui()->reset_requires_extra_frame(); - - _refresh_if_shown_on_screen(); - - if (m_extra_frame_requested || mouse3d_controller_applied || imgui_requires_extra_frame || wxGetApp().imgui()->requires_extra_frame()) { - m_extra_frame_requested = false; - evt.RequestMore(); - } - else - m_dirty = false; -} - -void GLCanvas3D::on_char(wxKeyEvent& evt) -{ - if (!m_initialized) - return; - - // see include/wx/defs.h enum wxKeyCode - int keyCode = evt.GetKeyCode(); - int ctrlMask = wxMOD_CONTROL; - int shiftMask = wxMOD_SHIFT; - - auto imgui = wxGetApp().imgui(); - if (imgui->update_key_data(evt)) { - render(); - return; - } - - if (keyCode == WXK_ESCAPE && (_deactivate_undo_redo_toolbar_items() || _deactivate_search_toolbar_item() || _deactivate_arrange_menu())) - return; - - if (m_gizmos.on_char(evt)) - return; - - if ((evt.GetModifiers() & ctrlMask) != 0) { - // CTRL is pressed - switch (keyCode) { -#ifdef __APPLE__ - case 'a': - case 'A': -#else /* __APPLE__ */ - case WXK_CONTROL_A: -#endif /* __APPLE__ */ - post_event(SimpleEvent(EVT_GLCANVAS_SELECT_ALL)); - break; -#ifdef __APPLE__ - case 'c': - case 'C': -#else /* __APPLE__ */ - case WXK_CONTROL_C: -#endif /* __APPLE__ */ - post_event(SimpleEvent(EVT_GLTOOLBAR_COPY)); - break; -#ifdef __APPLE__ - case 'm': - case 'M': -#else /* __APPLE__ */ - case WXK_CONTROL_M: -#endif /* __APPLE__ */ - { -#ifdef _WIN32 - if (wxGetApp().app_config->get("use_legacy_3DConnexion") == "1") { -#endif //_WIN32 -#ifdef __APPLE__ - // On OSX use Cmd+Shift+M to "Show/Hide 3Dconnexion devices settings dialog" - if ((evt.GetModifiers() & shiftMask) != 0) { -#endif // __APPLE__ - Mouse3DController& controller = wxGetApp().plater()->get_mouse3d_controller(); - controller.show_settings_dialog(!controller.is_settings_dialog_shown()); - m_dirty = true; -#ifdef __APPLE__ - } - else - // and Cmd+M to minimize application - wxGetApp().mainframe->Iconize(); -#endif // __APPLE__ -#ifdef _WIN32 - } -#endif //_WIN32 - break; - } -#ifdef __APPLE__ - case 'v': - case 'V': -#else /* __APPLE__ */ - case WXK_CONTROL_V: -#endif /* __APPLE__ */ - post_event(SimpleEvent(EVT_GLTOOLBAR_PASTE)); - break; - - -#ifdef __APPLE__ - case 'f': - case 'F': -#else /* __APPLE__ */ - case WXK_CONTROL_F: -#endif /* __APPLE__ */ - _activate_search_toolbar_item(); - break; - - -#ifdef __APPLE__ - case 'y': - case 'Y': -#else /* __APPLE__ */ - case WXK_CONTROL_Y: -#endif /* __APPLE__ */ - post_event(SimpleEvent(EVT_GLCANVAS_REDO)); - break; -#ifdef __APPLE__ - case 'z': - case 'Z': -#else /* __APPLE__ */ - case WXK_CONTROL_Z: -#endif /* __APPLE__ */ - post_event(SimpleEvent(EVT_GLCANVAS_UNDO)); - break; - - case WXK_BACK: - case WXK_DELETE: - post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE_ALL)); break; - default: evt.Skip(); - } - } else { - switch (keyCode) - { - case WXK_BACK: - case WXK_DELETE: { post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE)); break; } - case WXK_ESCAPE: { deselect_all(); break; } - case WXK_F5: { - if ((wxGetApp().is_editor() && !wxGetApp().plater()->model().objects.empty()) || - (wxGetApp().is_gcode_viewer() && !wxGetApp().plater()->get_last_loaded_gcode().empty())) - post_event(SimpleEvent(EVT_GLCANVAS_RELOAD_FROM_DISK)); - break; - } - case '0': { select_view("iso"); break; } - case '1': { select_view("top"); break; } - case '2': { select_view("bottom"); break; } - case '3': { select_view("front"); break; } - case '4': { select_view("rear"); break; } - case '5': { select_view("left"); break; } - case '6': { select_view("right"); break; } - case '+': { - if (dynamic_cast(m_canvas->GetParent()) != nullptr) - post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt)); - else - post_event(Event(EVT_GLCANVAS_INCREASE_INSTANCES, +1)); - break; - } - case '-': { - if (dynamic_cast(m_canvas->GetParent()) != nullptr) - post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt)); - else - post_event(Event(EVT_GLCANVAS_INCREASE_INSTANCES, -1)); - break; - } - case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; } - case 'A': - case 'a': { post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE)); break; } - case 'B': - case 'b': { zoom_to_bed(); break; } - case 'C': - case 'c': { m_gcode_viewer.toggle_gcode_window_visibility(); m_dirty = true; request_extra_frame(); break; } - case 'E': - case 'e': { m_labels.show(!m_labels.is_shown()); m_dirty = true; break; } - case 'G': - case 'g': { - if ((evt.GetModifiers() & shiftMask) != 0) { - if (dynamic_cast(m_canvas->GetParent()) != nullptr) - post_event(wxKeyEvent(EVT_GLCANVAS_JUMP_TO, evt)); - } - break; - } - case 'I': - case 'i': { _update_camera_zoom(1.0); break; } - case 'K': - case 'k': { wxGetApp().plater()->get_camera().select_next_type(); m_dirty = true; break; } - case 'L': - case 'l': { - if (!m_main_toolbar.is_enabled()) - show_legend(!is_legend_shown()); - break; - } - case 'O': - case 'o': { _update_camera_zoom(-1.0); break; } - case 'Z': - case 'z': { - if (!m_selection.is_empty()) - zoom_to_selection(); - else { - if (!m_volumes.empty()) - zoom_to_volumes(); - else - _zoom_to_box(m_gcode_viewer.get_paths_bounding_box()); - } - break; - } - default: { evt.Skip(); break; } - } - } -} - -class TranslationProcessor -{ - using UpAction = std::function; - using DownAction = std::function; - - UpAction m_up_action{ nullptr }; - DownAction m_down_action{ nullptr }; - - bool m_running{ false }; - Vec3d m_direction{ Vec3d::UnitX() }; - -public: - TranslationProcessor(UpAction up_action, DownAction down_action) - : m_up_action(up_action), m_down_action(down_action) - { - } - - void process(wxKeyEvent& evt) - { - const int keyCode = evt.GetKeyCode(); - wxEventType type = evt.GetEventType(); - if (type == wxEVT_KEY_UP) { - switch (keyCode) - { - case WXK_NUMPAD_LEFT: case WXK_LEFT: - case WXK_NUMPAD_RIGHT: case WXK_RIGHT: - case WXK_NUMPAD_UP: case WXK_UP: - case WXK_NUMPAD_DOWN: case WXK_DOWN: - { - m_running = false; - m_up_action(); - break; - } - default: { break; } - } - } - else if (type == wxEVT_KEY_DOWN) { - bool apply = false; - - switch (keyCode) - { - case WXK_SHIFT: - { - if (m_running) - apply = true; - - break; - } - case WXK_NUMPAD_LEFT: - case WXK_LEFT: - { - m_direction = -Vec3d::UnitX(); - apply = true; - break; - } - case WXK_NUMPAD_RIGHT: - case WXK_RIGHT: - { - m_direction = Vec3d::UnitX(); - apply = true; - break; - } - case WXK_NUMPAD_UP: - case WXK_UP: - { - m_direction = Vec3d::UnitY(); - apply = true; - break; - } - case WXK_NUMPAD_DOWN: - case WXK_DOWN: - { - m_direction = -Vec3d::UnitY(); - apply = true; - break; - } - default: { break; } - } - - if (apply) { - m_running = true; - m_down_action(m_direction, evt.ShiftDown(), evt.CmdDown()); - } - } - } -}; - -void GLCanvas3D::on_key(wxKeyEvent& evt) -{ - static TranslationProcessor translationProcessor( - [this]() { - do_move(L("Gizmo-Move")); - m_gizmos.update_data(); - - wxGetApp().obj_manipul()->set_dirty(); - // Let the plater know that the dragging finished, so a delayed refresh - // of the scene with the background processing data should be performed. - post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); - // updates camera target constraints - refresh_camera_scene_box(); - m_dirty = true; - }, - [this](const Vec3d& direction, bool slow, bool camera_space) { - m_selection.setup_cache(); - double multiplier = slow ? 1.0 : 10.0; - - Vec3d displacement; - if (camera_space) { - Eigen::Matrix inv_view_3x3 = wxGetApp().plater()->get_camera().get_view_matrix().inverse().matrix().block(0, 0, 3, 3); - displacement = multiplier * (inv_view_3x3 * direction); - displacement.z() = 0.0; - } - else - displacement = multiplier * direction; - -#if ENABLE_WORLD_COORDINATE - TransformationType trafo_type; - trafo_type.set_relative(); - m_selection.translate(displacement, trafo_type); -#else - m_selection.translate(displacement); -#endif // ENABLE_WORLD_COORDINATE - m_dirty = true; - } - ); - - const int keyCode = evt.GetKeyCode(); - - auto imgui = wxGetApp().imgui(); - if (imgui->update_key_data(evt)) { - render(); - } - else { - if (!m_gizmos.on_key(evt)) { - if (evt.GetEventType() == wxEVT_KEY_UP) { - if (evt.ShiftDown() && evt.ControlDown() && keyCode == WXK_SPACE) { - wxGetApp().plater()->toggle_render_statistic_dialog(); - m_dirty = true; - } - if (m_tab_down && keyCode == WXK_TAB && !evt.HasAnyModifiers()) { - // Enable switching between 3D and Preview with Tab - // m_canvas->HandleAsNavigationKey(evt); // XXX: Doesn't work in some cases / on Linux - post_event(SimpleEvent(EVT_GLCANVAS_TAB)); - } - else if (keyCode == WXK_TAB && evt.ShiftDown() && ! wxGetApp().is_gcode_viewer()) { - // Collapse side-panel with Shift+Tab - post_event(SimpleEvent(EVT_GLCANVAS_COLLAPSE_SIDEBAR)); - } - else if (keyCode == WXK_SHIFT) { - translationProcessor.process(evt); - - if (m_picking_enabled && m_rectangle_selection.is_dragging()) { - _update_selection_from_hover(); - m_rectangle_selection.stop_dragging(); - m_mouse.ignore_left_up = true; - } - m_shift_kar_filter.reset_count(); - m_dirty = true; -// set_cursor(Standard); - } - else if (keyCode == WXK_ALT) { - if (m_picking_enabled && m_rectangle_selection.is_dragging()) { - _update_selection_from_hover(); - m_rectangle_selection.stop_dragging(); - m_mouse.ignore_left_up = true; - m_dirty = true; - } -// set_cursor(Standard); - } - else if (keyCode == WXK_CONTROL) { -#if ENABLE_NEW_CAMERA_MOVEMENTS -#if ENABLE_RAYCAST_PICKING - if (m_mouse.dragging && !m_moving) { -#else - if (m_mouse.dragging) { -#endif // ENABLE_RAYCAST_PICKING - // if the user releases CTRL while rotating the 3D scene - // prevent from moving the selected volume - m_mouse.drag.move_volume_idx = -1; - m_mouse.set_start_position_3D_as_invalid(); - } -#endif // ENABLE_NEW_CAMERA_MOVEMENTS - m_ctrl_kar_filter.reset_count(); - m_dirty = true; - } - else if (m_gizmos.is_enabled() && !m_selection.is_empty()) { - translationProcessor.process(evt); - - switch (keyCode) - { - case WXK_NUMPAD_PAGEUP: case WXK_PAGEUP: - case WXK_NUMPAD_PAGEDOWN: case WXK_PAGEDOWN: - { - do_rotate(L("Gizmo-Rotate")); - m_gizmos.update_data(); - - wxGetApp().obj_manipul()->set_dirty(); - // Let the plater know that the dragging finished, so a delayed refresh - // of the scene with the background processing data should be performed. - post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); - // updates camera target constraints - refresh_camera_scene_box(); - m_dirty = true; - - break; - } - default: { break; } - } - } - } - else if (evt.GetEventType() == wxEVT_KEY_DOWN) { - m_tab_down = keyCode == WXK_TAB && !evt.HasAnyModifiers(); - if (keyCode == WXK_SHIFT) { - translationProcessor.process(evt); - - if (m_picking_enabled && (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports)) { - m_mouse.ignore_left_up = false; -// set_cursor(Cross); - } - if (m_shift_kar_filter.is_first()) - m_dirty = true; - - m_shift_kar_filter.increase_count(); - } - else if (keyCode == WXK_ALT) { - if (m_picking_enabled && (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports)) { - m_mouse.ignore_left_up = false; -// set_cursor(Cross); - } - } - else if (keyCode == WXK_CONTROL) { - if (m_ctrl_kar_filter.is_first()) - m_dirty = true; - - m_ctrl_kar_filter.increase_count(); - } - else if (m_gizmos.is_enabled() && !m_selection.is_empty()) { - auto do_rotate = [this](double angle_z_rad) { - m_selection.setup_cache(); - m_selection.rotate(Vec3d(0.0, 0.0, angle_z_rad), TransformationType(TransformationType::World_Relative_Joint)); - m_dirty = true; -// wxGetApp().obj_manipul()->set_dirty(); - }; - - translationProcessor.process(evt); - - switch (keyCode) - { - case WXK_NUMPAD_PAGEUP: case WXK_PAGEUP: { do_rotate(0.25 * M_PI); break; } - case WXK_NUMPAD_PAGEDOWN: case WXK_PAGEDOWN: { do_rotate(-0.25 * M_PI); break; } - default: { break; } - } - } - else if (!m_gizmos.is_enabled()) { - // DoubleSlider navigation in Preview - if (keyCode == WXK_LEFT || - keyCode == WXK_RIGHT || - keyCode == WXK_UP || - keyCode == WXK_DOWN) { - if (dynamic_cast(m_canvas->GetParent()) != nullptr) - post_event(wxKeyEvent(EVT_GLCANVAS_MOVE_SLIDERS, evt)); - } - } - } - } - } - - if (keyCode != WXK_TAB - && keyCode != WXK_LEFT - && keyCode != WXK_UP - && keyCode != WXK_RIGHT - && keyCode != WXK_DOWN) { - evt.Skip(); // Needed to have EVT_CHAR generated as well - } -} - -void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt) -{ -#ifdef WIN32 - // Try to filter out spurious mouse wheel events comming from 3D mouse. - if (wxGetApp().plater()->get_mouse3d_controller().process_mouse_wheel()) - return; -#endif - - if (!m_initialized) - return; - - // Ignore the wheel events if the middle button is pressed. - if (evt.MiddleIsDown()) - return; - -#if ENABLE_RETINA_GL - const float scale = m_retina_helper->get_scale_factor(); - evt.SetX(evt.GetX() * scale); - evt.SetY(evt.GetY() * scale); -#endif - - if (wxGetApp().imgui()->update_mouse_data(evt)) { - m_dirty = true; - return; - } - -#ifdef __WXMSW__ - // For some reason the Idle event is not being generated after the mouse scroll event in case of scrolling with the two fingers on the touch pad, - // if the event is not allowed to be passed further. - // https://github.com/prusa3d/PrusaSlicer/issues/2750 - // evt.Skip() used to trigger the needed screen refresh, but it does no more. wxWakeUpIdle() seem to work now. - wxWakeUpIdle(); -#endif /* __WXMSW__ */ - - // Performs layers editing updates, if enabled - if (is_layers_editing_enabled()) { - int object_idx_selected = m_selection.get_object_idx(); - if (object_idx_selected != -1) { - // A volume is selected. Test, whether hovering over a layer thickness bar. - if (m_layers_editing.bar_rect_contains(*this, (float)evt.GetX(), (float)evt.GetY())) { - // Adjust the width of the selection. - m_layers_editing.band_width = std::max(std::min(m_layers_editing.band_width * (1.0f + 0.1f * (float)evt.GetWheelRotation() / (float)evt.GetWheelDelta()), 10.0f), 1.5f); - if (m_canvas != nullptr) - m_canvas->Refresh(); - - return; - } - } - } - - // If the Search window or Undo/Redo list is opened, - // update them according to the event - if (m_main_toolbar.is_item_pressed("search") || - m_undoredo_toolbar.is_item_pressed("undo") || - m_undoredo_toolbar.is_item_pressed("redo")) { - m_mouse_wheel = int((double)evt.GetWheelRotation() / (double)evt.GetWheelDelta()); - return; - } - - // Inform gizmos about the event so they have the opportunity to react. - if (m_gizmos.on_mouse_wheel(evt)) - return; - - // Calculate the zoom delta and apply it to the current zoom factor - double direction_factor = (wxGetApp().app_config->get("reverse_mouse_wheel_zoom") == "1") ? -1.0 : 1.0; - _update_camera_zoom(direction_factor * (double)evt.GetWheelRotation() / (double)evt.GetWheelDelta()); -} - -void GLCanvas3D::on_timer(wxTimerEvent& evt) -{ - if (m_layers_editing.state == LayersEditing::Editing) - _perform_layer_editing_action(); -} - -void GLCanvas3D::on_render_timer(wxTimerEvent& evt) -{ - // no need to wake up idle - // right after this event, idle event is fired - // m_dirty = true; - // wxWakeUpIdle(); -} - - -void GLCanvas3D::schedule_extra_frame(int miliseconds) -{ - // Schedule idle event right now - if (miliseconds == 0) - { - // We want to wakeup idle evnt but most likely this is call inside render cycle so we need to wait - if (m_in_render) - miliseconds = 33; - else { - m_dirty = true; - wxWakeUpIdle(); - return; - } - } - int remaining_time = m_render_timer.GetInterval(); - // Timer is not running - if (!m_render_timer.IsRunning()) { - m_render_timer.StartOnce(miliseconds); - // Timer is running - restart only if new period is shorter than remaning period - } else { - if (miliseconds + 20 < remaining_time) { - m_render_timer.Stop(); - m_render_timer.StartOnce(miliseconds); - } - } -} - -#ifndef NDEBUG -// #define SLIC3R_DEBUG_MOUSE_EVENTS -#endif - -#ifdef SLIC3R_DEBUG_MOUSE_EVENTS -std::string format_mouse_event_debug_message(const wxMouseEvent &evt) -{ - static int idx = 0; - char buf[2048]; - std::string out; - sprintf(buf, "Mouse Event %d - ", idx ++); - out = buf; - - if (evt.Entering()) - out += "Entering "; - if (evt.Leaving()) - out += "Leaving "; - if (evt.Dragging()) - out += "Dragging "; - if (evt.Moving()) - out += "Moving "; - if (evt.Magnify()) - out += "Magnify "; - if (evt.LeftDown()) - out += "LeftDown "; - if (evt.LeftUp()) - out += "LeftUp "; - if (evt.LeftDClick()) - out += "LeftDClick "; - if (evt.MiddleDown()) - out += "MiddleDown "; - if (evt.MiddleUp()) - out += "MiddleUp "; - if (evt.MiddleDClick()) - out += "MiddleDClick "; - if (evt.RightDown()) - out += "RightDown "; - if (evt.RightUp()) - out += "RightUp "; - if (evt.RightDClick()) - out += "RightDClick "; - - sprintf(buf, "(%d, %d)", evt.GetX(), evt.GetY()); - out += buf; - return out; -} -#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */ - -void GLCanvas3D::on_mouse(wxMouseEvent& evt) -{ - if (!m_initialized || !_set_current()) - return; - -#if ENABLE_RETINA_GL - const float scale = m_retina_helper->get_scale_factor(); - evt.SetX(evt.GetX() * scale); - evt.SetY(evt.GetY() * scale); -#endif - - Point pos(evt.GetX(), evt.GetY()); - - ImGuiWrapper* imgui = wxGetApp().imgui(); - if (m_tooltip.is_in_imgui() && evt.LeftUp()) - // ignore left up events coming from imgui windows and not processed by them - m_mouse.ignore_left_up = true; - m_tooltip.set_in_imgui(false); - if (imgui->update_mouse_data(evt)) { - m_mouse.position = evt.Leaving() ? Vec2d(-1.0, -1.0) : pos.cast(); - m_tooltip.set_in_imgui(true); - render(); -#ifdef SLIC3R_DEBUG_MOUSE_EVENTS - printf((format_mouse_event_debug_message(evt) + " - Consumed by ImGUI\n").c_str()); -#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */ - m_dirty = true; - // do not return if dragging or tooltip not empty to allow for tooltip update - // also, do not return if the mouse is moving and also is inside MM gizmo to allow update seed fill selection - if (!m_mouse.dragging && m_tooltip.is_empty() && (m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation || !evt.Moving())) - return; - } - -#ifdef __WXMSW__ - bool on_enter_workaround = false; - if (! evt.Entering() && ! evt.Leaving() && m_mouse.position.x() == -1.0) { - // Workaround for SPE-832: There seems to be a mouse event sent to the window before evt.Entering() - m_mouse.position = pos.cast(); - render(); -#ifdef SLIC3R_DEBUG_MOUSE_EVENTS - printf((format_mouse_event_debug_message(evt) + " - OnEnter workaround\n").c_str()); -#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */ - on_enter_workaround = true; - } else -#endif /* __WXMSW__ */ - { -#ifdef SLIC3R_DEBUG_MOUSE_EVENTS - printf((format_mouse_event_debug_message(evt) + " - other\n").c_str()); -#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */ - } - - if (m_main_toolbar.on_mouse(evt, *this)) { - if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) - mouse_up_cleanup(); - m_mouse.set_start_position_3D_as_invalid(); - return; - } - - if (m_undoredo_toolbar.on_mouse(evt, *this)) { - if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) - mouse_up_cleanup(); - m_mouse.set_start_position_3D_as_invalid(); - return; - } - - if (wxGetApp().plater()->get_collapse_toolbar().on_mouse(evt, *this)) { - if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) - mouse_up_cleanup(); - m_mouse.set_start_position_3D_as_invalid(); - return; - } - - if (wxGetApp().plater()->get_view_toolbar().on_mouse(evt, *this)) { - if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) - mouse_up_cleanup(); - m_mouse.set_start_position_3D_as_invalid(); - return; - } - - for (GLVolume* volume : m_volumes.volumes) { - volume->force_sinking_contours = false; - } - - auto show_sinking_contours = [this]() { - const Selection::IndicesList& idxs = m_selection.get_volume_idxs(); - for (unsigned int idx : idxs) { - m_volumes.volumes[idx]->force_sinking_contours = true; - } - m_dirty = true; - }; - - if (m_gizmos.on_mouse(evt)) { - if (wxWindow::FindFocus() != m_canvas) - // Grab keyboard focus for input in gizmo dialogs. - m_canvas->SetFocus(); - - if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) - mouse_up_cleanup(); - - m_mouse.set_start_position_3D_as_invalid(); - m_mouse.position = pos.cast(); - - // It should be detection of volume change - // Not only detection of some modifiers !!! - if (evt.Dragging()) { - GLGizmosManager::EType c = m_gizmos.get_current_type(); - if (current_printer_technology() == ptFFF && - fff_print()->config().complete_objects){ - if (c == GLGizmosManager::EType::Move || - c == GLGizmosManager::EType::Scale || - c == GLGizmosManager::EType::Rotate ) - update_sequential_clearance(); - } else { - if (c == GLGizmosManager::EType::Move || - c == GLGizmosManager::EType::Scale || - c == GLGizmosManager::EType::Rotate) - show_sinking_contours(); - } - } - - return; - } - - bool any_gizmo_active = m_gizmos.get_current() != nullptr; - - int selected_object_idx = m_selection.get_object_idx(); - int layer_editing_object_idx = is_layers_editing_enabled() ? selected_object_idx : -1; - m_layers_editing.select_object(*m_model, layer_editing_object_idx); - - if (m_mouse.drag.move_requires_threshold && m_mouse.is_move_start_threshold_position_2D_defined() && m_mouse.is_move_threshold_met(pos)) { - m_mouse.drag.move_requires_threshold = false; - m_mouse.set_move_start_threshold_position_2D_as_invalid(); - } - - if (evt.ButtonDown() && wxWindow::FindFocus() != m_canvas) - // Grab keyboard focus on any mouse click event. - m_canvas->SetFocus(); - - if (evt.Entering()) { -//#if defined(__WXMSW__) || defined(__linux__) -// // On Windows and Linux needs focus in order to catch key events - // Set focus in order to remove it from object list - if (m_canvas != nullptr) { - // Only set focus, if the top level window of this canvas is active - // and ObjectList has a focus - auto p = dynamic_cast(evt.GetEventObject()); - while (p->GetParent()) - p = p->GetParent(); -#ifdef __WIN32__ - wxWindow* const obj_list = wxGetApp().obj_list()->GetMainWindow(); -#else - wxWindow* const obj_list = wxGetApp().obj_list(); -#endif - if (obj_list == p->FindFocus()) - m_canvas->SetFocus(); - m_mouse.position = pos.cast(); - m_tooltip_enabled = false; - // 1) forces a frame render to ensure that m_hover_volume_idxs is updated even when the user right clicks while - // the context menu is shown, ensuring it to disappear if the mouse is outside any volume and to - // change the volume hover state if any is under the mouse - // 2) when switching between 3d view and preview the size of the canvas changes if the side panels are visible, - // so forces a resize to avoid multiple renders with different sizes (seen as flickering) - _refresh_if_shown_on_screen(); - m_tooltip_enabled = true; - } - m_mouse.set_start_position_2D_as_invalid(); -//#endif - } - else if (evt.Leaving()) { - _deactivate_undo_redo_toolbar_items(); - - // to remove hover on objects when the mouse goes out of this canvas - m_mouse.position = Vec2d(-1.0, -1.0); - m_dirty = true; - } - else if (evt.LeftDown() || evt.RightDown() || evt.MiddleDown()) { - if (_deactivate_undo_redo_toolbar_items() || _deactivate_search_toolbar_item() || _deactivate_arrange_menu()) - return; - - // If user pressed left or right button we first check whether this happened - // on a volume or not. - m_layers_editing.state = LayersEditing::Unknown; - if (layer_editing_object_idx != -1 && m_layers_editing.bar_rect_contains(*this, pos(0), pos(1))) { - // A volume is selected and the mouse is inside the layer thickness bar. - // Start editing the layer height. - m_layers_editing.state = LayersEditing::Editing; - _perform_layer_editing_action(&evt); - } - else { - const bool rectangle_selection_dragging = m_rectangle_selection.is_dragging(); - if (evt.LeftDown() && (evt.ShiftDown() || evt.AltDown()) && m_picking_enabled) { - if (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports && - m_gizmos.get_current_type() != GLGizmosManager::FdmSupports && - m_gizmos.get_current_type() != GLGizmosManager::Seam && - m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation) { - m_rectangle_selection.start_dragging(m_mouse.position, evt.ShiftDown() ? GLSelectionRectangle::EState::Select : GLSelectionRectangle::EState::Deselect); - m_dirty = true; - } - } - - // Select volume in this 3D canvas. - // Don't deselect a volume if layer editing is enabled or any gizmo is active. We want the object to stay selected - // during the scene manipulation. - - if (m_picking_enabled && (!any_gizmo_active || !evt.CmdDown()) && (!m_hover_volume_idxs.empty() || !is_layers_editing_enabled()) && !rectangle_selection_dragging) { - if (evt.LeftDown() && !m_hover_volume_idxs.empty()) { - int volume_idx = get_first_hover_volume_idx(); - bool already_selected = m_selection.contains_volume(volume_idx); - bool shift_down = evt.ShiftDown(); - - Selection::IndicesList curr_idxs = m_selection.get_volume_idxs(); - - if (already_selected && shift_down) - m_selection.remove(volume_idx); - else { - m_selection.add(volume_idx, !shift_down, true); - m_mouse.drag.move_requires_threshold = !already_selected; - if (already_selected) - m_mouse.set_move_start_threshold_position_2D_as_invalid(); - else - m_mouse.drag.move_start_threshold_position_2D = pos; - } - - // propagate event through callback - if (curr_idxs != m_selection.get_volume_idxs()) { - if (m_selection.is_empty()) - m_gizmos.reset_all_states(); - else - m_gizmos.refresh_on_off_state(); - - m_gizmos.update_data(); - post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); - m_dirty = true; - } - } - } - - if (!m_hover_volume_idxs.empty() && !m_rectangle_selection.is_dragging()) { - if (evt.LeftDown() && m_moving_enabled && m_mouse.drag.move_volume_idx == -1) { - // Only accept the initial position, if it is inside the volume bounding box. - const int volume_idx = get_first_hover_volume_idx(); - BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box(); - volume_bbox.offset(1.0); - if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(m_mouse.scene_position)) { - m_volumes.volumes[volume_idx]->hover = GLVolume::HS_None; - // The dragging operation is initiated. - m_mouse.drag.move_volume_idx = volume_idx; -#if ENABLE_NEW_CAMERA_MOVEMENTS - m_selection.setup_cache(); - if (!evt.CmdDown()) -#endif // ENABLE_NEW_CAMERA_MOVEMENTS - m_mouse.drag.start_position_3D = m_mouse.scene_position; - m_sequential_print_clearance_first_displacement = true; -#if !ENABLE_RAYCAST_PICKING - m_moving = true; -#endif // !ENABLE_RAYCAST_PICKING - } - } - } - } - } -#if ENABLE_NEW_CAMERA_MOVEMENTS - else if (evt.Dragging() && evt.LeftIsDown() && !evt.CmdDown() && m_layers_editing.state == LayersEditing::Unknown && - m_mouse.drag.move_volume_idx != -1 && m_mouse.is_start_position_3D_defined()) { -#else - else if (evt.Dragging() && evt.LeftIsDown() && m_layers_editing.state == LayersEditing::Unknown && m_mouse.drag.move_volume_idx != -1) { -#endif // ENABLE_NEW_CAMERA_MOVEMENTS - if (!m_mouse.drag.move_requires_threshold) { - m_mouse.dragging = true; - Vec3d cur_pos = m_mouse.drag.start_position_3D; - // we do not want to translate objects if the user just clicked on an object while pressing shift to remove it from the selection and then drag - if (m_selection.contains_volume(get_first_hover_volume_idx())) { - const Camera& camera = wxGetApp().plater()->get_camera(); - if (std::abs(camera.get_dir_forward().z()) < EPSILON) { - // side view -> move selected volumes orthogonally to camera view direction - const Linef3 ray = mouse_ray(pos); - const Vec3d dir = ray.unit_vector(); - // finds the intersection of the mouse ray with the plane parallel to the camera viewport and passing throught the starting position - // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebric form - // in our case plane normal and ray direction are the same (orthogonal view) - // when moving to perspective camera the negative z unit axis of the camera needs to be transformed in world space and used as plane normal - const Vec3d inters = ray.a + (m_mouse.drag.start_position_3D - ray.a).dot(dir) / dir.squaredNorm() * dir; - // vector from the starting position to the found intersection - const Vec3d inters_vec = inters - m_mouse.drag.start_position_3D; - - const Vec3d camera_right = camera.get_dir_right(); - const Vec3d camera_up = camera.get_dir_up(); - - // finds projection of the vector along the camera axes - const double projection_x = inters_vec.dot(camera_right); - const double projection_z = inters_vec.dot(camera_up); - - // apply offset - cur_pos = m_mouse.drag.start_position_3D + projection_x * camera_right + projection_z * camera_up; - } - else { - // Generic view - // Get new position at the same Z of the initial click point. - cur_pos = mouse_ray(pos).intersect_plane(m_mouse.drag.start_position_3D.z()); - } - } - -#if ENABLE_WORLD_COORDINATE -#if ENABLE_RAYCAST_PICKING - m_moving = true; -#endif // ENABLE_RAYCAST_PICKING - TransformationType trafo_type; - trafo_type.set_relative(); - m_selection.translate(cur_pos - m_mouse.drag.start_position_3D, trafo_type); -#else - m_selection.translate(cur_pos - m_mouse.drag.start_position_3D); -#endif // ENABLE_WORLD_COORDINATE - if (current_printer_technology() == ptFFF && fff_print()->config().complete_objects) - update_sequential_clearance(); - wxGetApp().obj_manipul()->set_dirty(); - m_dirty = true; - } - } - else if (evt.Dragging() && evt.LeftIsDown() && m_picking_enabled && m_rectangle_selection.is_dragging()) { - // keeps the mouse position updated while dragging the selection rectangle - m_mouse.position = pos.cast(); - m_rectangle_selection.dragging(m_mouse.position); - m_dirty = true; - } - else if (evt.Dragging()) { - m_mouse.dragging = true; - - if (m_layers_editing.state != LayersEditing::Unknown && layer_editing_object_idx != -1) { - if (m_layers_editing.state == LayersEditing::Editing) { - _perform_layer_editing_action(&evt); - m_mouse.position = pos.cast(); - } - } - // do not process the dragging if the left mouse was set down in another canvas -#if ENABLE_NEW_CAMERA_MOVEMENTS - else if (evt.LeftIsDown()) { - // if dragging over blank area with left button, rotate -#if ENABLE_RAYCAST_PICKING - if (!m_moving) { -#endif // ENABLE_RAYCAST_PICKING - if ((any_gizmo_active || evt.CmdDown() || m_hover_volume_idxs.empty()) && m_mouse.is_start_position_3D_defined()) { -#else - // if dragging over blank area with left button, rotate - else if (evt.LeftIsDown()) { - if ((any_gizmo_active || m_hover_volume_idxs.empty()) && m_mouse.is_start_position_3D_defined()) { -#endif // ENABLE_NEW_CAMERA_MOVEMENTS - const Vec3d rot = (Vec3d(pos.x(), pos.y(), 0.0) - m_mouse.drag.start_position_3D) * (PI * TRACKBALLSIZE / 180.0); - if (wxGetApp().app_config->get("use_free_camera") == "1") - // Virtual track ball (similar to the 3DConnexion mouse). - wxGetApp().plater()->get_camera().rotate_local_around_target(Vec3d(rot.y(), rot.x(), 0.0)); - else { - // Forces camera right vector to be parallel to XY plane in case it has been misaligned using the 3D mouse free rotation. - // It is cheaper to call this function right away instead of testing wxGetApp().plater()->get_mouse3d_controller().connected(), - // which checks an atomics (flushes CPU caches). - // See GH issue #3816. - Camera& camera = wxGetApp().plater()->get_camera(); - camera.recover_from_free_camera(); - camera.rotate_on_sphere(rot.x(), rot.y(), current_printer_technology() != ptSLA); - } - - m_dirty = true; - } - m_mouse.drag.start_position_3D = Vec3d((double)pos.x(), (double)pos.y(), 0.0); -#if ENABLE_RAYCAST_PICKING - } -#endif // ENABLE_RAYCAST_PICKING - } - else if (evt.MiddleIsDown() || evt.RightIsDown()) { - // If dragging over blank area with right/middle button, pan. - if (m_mouse.is_start_position_2D_defined()) { - // get point in model space at Z = 0 - float z = 0.0f; - const Vec3d cur_pos = _mouse_to_3d(pos, &z); - const Vec3d orig = _mouse_to_3d(m_mouse.drag.start_position_2D, &z); - Camera& camera = wxGetApp().plater()->get_camera(); - if (wxGetApp().app_config->get("use_free_camera") != "1") - // Forces camera right vector to be parallel to XY plane in case it has been misaligned using the 3D mouse free rotation. - // It is cheaper to call this function right away instead of testing wxGetApp().plater()->get_mouse3d_controller().connected(), - // which checks an atomics (flushes CPU caches). - // See GH issue #3816. - camera.recover_from_free_camera(); - - camera.set_target(camera.get_target() + orig - cur_pos); - m_dirty = true; - } - - m_mouse.drag.start_position_2D = pos; - } - } - else if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) { -#if ENABLE_RAYCAST_PICKING - m_mouse.position = pos.cast(); -#endif // ENABLE_RAYCAST_PICKING - - if (m_layers_editing.state != LayersEditing::Unknown) { - m_layers_editing.state = LayersEditing::Unknown; - _stop_timer(); - m_layers_editing.accept_changes(*this); - } - else if (m_mouse.drag.move_volume_idx != -1 && m_mouse.dragging) { - do_move(L("Move Object")); - wxGetApp().obj_manipul()->set_dirty(); - // Let the plater know that the dragging finished, so a delayed refresh - // of the scene with the background processing data should be performed. - post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); - } - else if (evt.LeftUp() && m_picking_enabled && m_rectangle_selection.is_dragging()) { - if (evt.ShiftDown() || evt.AltDown()) - _update_selection_from_hover(); - - m_rectangle_selection.stop_dragging(); - } - else if (evt.LeftUp() && !m_mouse.ignore_left_up && !m_mouse.dragging && m_hover_volume_idxs.empty() && !is_layers_editing_enabled()) { - // deselect and propagate event through callback - if (!evt.ShiftDown() && (!any_gizmo_active || !evt.CmdDown()) && m_picking_enabled) - deselect_all(); - } - else if (evt.RightUp()) { -#if !ENABLE_RAYCAST_PICKING - m_mouse.position = pos.cast(); -#endif // !ENABLE_RAYCAST_PICKING - // forces a frame render to ensure that m_hover_volume_idxs is updated even when the user right clicks while - // the context menu is already shown - render(); - if (!m_hover_volume_idxs.empty()) { - // if right clicking on volume, propagate event through callback (shows context menu) - int volume_idx = get_first_hover_volume_idx(); - if (!m_volumes.volumes[volume_idx]->is_wipe_tower // no context menu for the wipe tower - && m_gizmos.get_current_type() != GLGizmosManager::SlaSupports) // disable context menu when the gizmo is open - { - // forces the selection of the volume - /* m_selection.add(volume_idx); // #et_FIXME_if_needed - * To avoid extra "Add-Selection" snapshots, - * call add() with check_for_already_contained=true - * */ - m_selection.add(volume_idx, true, true); - m_gizmos.refresh_on_off_state(); - post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); - m_gizmos.update_data(); - wxGetApp().obj_manipul()->set_dirty(); - // forces a frame render to update the view before the context menu is shown - render(); - } - } - Vec2d logical_pos = pos.cast(); -#if ENABLE_RETINA_GL - const float factor = m_retina_helper->get_scale_factor(); - logical_pos = logical_pos.cwiseQuotient(Vec2d(factor, factor)); -#endif // ENABLE_RETINA_GL - if (!m_mouse.dragging) { - // do not post the event if the user is panning the scene - // or if right click was done over the wipe tower - const bool post_right_click_event = m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower; - if (post_right_click_event) - post_event(RBtnEvent(EVT_GLCANVAS_RIGHT_CLICK, { logical_pos, m_hover_volume_idxs.empty() })); - } - } - - mouse_up_cleanup(); - } - else if (evt.Moving()) { - m_mouse.position = pos.cast(); - - // updates gizmos overlay - if (m_selection.is_empty()) - m_gizmos.reset_all_states(); - - m_dirty = true; - } - else - evt.Skip(); - - if (m_moving) - show_sinking_contours(); - -#ifdef __WXMSW__ - if (on_enter_workaround) - m_mouse.position = Vec2d(-1., -1.); -#endif /* __WXMSW__ */ -} - -void GLCanvas3D::on_paint(wxPaintEvent& evt) -{ - if (m_initialized) - m_dirty = true; - else - // Call render directly, so it gets initialized immediately, not from On Idle handler. - this->render(); -} - -void GLCanvas3D::on_set_focus(wxFocusEvent& evt) -{ - m_tooltip_enabled = false; - _refresh_if_shown_on_screen(); - m_tooltip_enabled = true; -} - -Size GLCanvas3D::get_canvas_size() const -{ - int w = 0; - int h = 0; - - if (m_canvas != nullptr) - m_canvas->GetSize(&w, &h); - -#if ENABLE_RETINA_GL - const float factor = m_retina_helper->get_scale_factor(); - w *= factor; - h *= factor; -#else - const float factor = 1.0f; -#endif - - return Size(w, h, factor); -} - -Vec2d GLCanvas3D::get_local_mouse_position() const -{ - if (m_canvas == nullptr) - return Vec2d::Zero(); - - wxPoint mouse_pos = m_canvas->ScreenToClient(wxGetMousePosition()); - const double factor = -#if ENABLE_RETINA_GL - m_retina_helper->get_scale_factor(); -#else - 1.0; -#endif - return Vec2d(factor * mouse_pos.x, factor * mouse_pos.y); -} - -void GLCanvas3D::set_tooltip(const std::string& tooltip) -{ - if (m_canvas != nullptr) - m_tooltip.set_text(tooltip); -} - -void GLCanvas3D::do_move(const std::string& snapshot_type) -{ - if (m_model == nullptr) - return; - - if (!snapshot_type.empty()) - wxGetApp().plater()->take_snapshot(_(snapshot_type)); - - std::set> done; // keeps track of modified instances - bool object_moved = false; - Vec3d wipe_tower_origin = Vec3d::Zero(); - - Selection::EMode selection_mode = m_selection.get_mode(); - - for (const GLVolume* v : m_volumes.volumes) { - int object_idx = v->object_idx(); - int instance_idx = v->instance_idx(); - int volume_idx = v->volume_idx(); - - std::pair done_id(object_idx, instance_idx); - - if (0 <= object_idx && object_idx < (int)m_model->objects.size()) { - done.insert(done_id); - - // Move instances/volumes - ModelObject* model_object = m_model->objects[object_idx]; - if (model_object != nullptr) { - if (selection_mode == Selection::Instance) -#if ENABLE_WORLD_COORDINATE - model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); -#else - model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); -#endif // ENABLE_WORLD_COORDINATE - else if (selection_mode == Selection::Volume) -#if ENABLE_WORLD_COORDINATE - model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); -#else - model_object->volumes[volume_idx]->set_offset(v->get_volume_offset()); -#endif // ENABLE_WORLD_COORDINATE - - object_moved = true; - model_object->invalidate_bounding_box(); - } - } - else if (v->is_wipe_tower) - // Move a wipe tower proxy. - wipe_tower_origin = v->get_volume_offset(); - } - - // Fixes flying instances - for (const std::pair& i : done) { - ModelObject* m = m_model->objects[i.first]; - const double shift_z = m->get_instance_min_z(i.second); - if (current_printer_technology() == ptSLA || shift_z > SINKING_Z_THRESHOLD) { - const Vec3d shift(0.0, 0.0, -shift_z); - m_selection.translate(i.first, i.second, shift); - m->translate_instance(i.second, shift); - } - wxGetApp().obj_list()->update_info_items(static_cast(i.first)); - } - - // if the selection is not valid to allow for layer editing after the move, we need to turn off the tool if it is running - // similar to void Plater::priv::selection_changed() - if (!wxGetApp().plater()->can_layers_editing() && is_layers_editing_enabled()) - post_event(SimpleEvent(EVT_GLTOOLBAR_LAYERSEDITING)); - - if (object_moved) - post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED)); - - if (wipe_tower_origin != Vec3d::Zero()) - post_event(Vec3dEvent(EVT_GLCANVAS_WIPETOWER_MOVED, std::move(wipe_tower_origin))); - - reset_sequential_print_clearance(); - - m_dirty = true; -} - -void GLCanvas3D::do_rotate(const std::string& snapshot_type) -{ - if (m_model == nullptr) - return; - - if (!snapshot_type.empty()) - wxGetApp().plater()->take_snapshot(_(snapshot_type)); - - // stores current min_z of instances - std::map, double> min_zs; - for (int i = 0; i < static_cast(m_model->objects.size()); ++i) { - const ModelObject* obj = m_model->objects[i]; - for (int j = 0; j < static_cast(obj->instances.size()); ++j) { - if (snapshot_type == L("Gizmo-Place on Face") && m_selection.get_object_idx() == i) { - // This means we are flattening this object. In that case pretend - // that it is not sinking (even if it is), so it is placed on bed - // later on (whatever is sinking will be left sinking). - min_zs[{ i, j }] = SINKING_Z_THRESHOLD; - } - else - min_zs[{ i, j }] = obj->instance_bounding_box(j).min.z(); - - } - } - - std::set> done; // keeps track of modified instances - - Selection::EMode selection_mode = m_selection.get_mode(); - - for (const GLVolume* v : m_volumes.volumes) { - if (v->is_wipe_tower) { - const Vec3d offset = v->get_volume_offset(); - post_event(Vec3dEvent(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3d(offset.x(), offset.y(), v->get_volume_rotation().z()))); - } - const int object_idx = v->object_idx(); - if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) - continue; - - const int instance_idx = v->instance_idx(); - const int volume_idx = v->volume_idx(); - - done.insert(std::pair(object_idx, instance_idx)); - - // Rotate instances/volumes. - ModelObject* model_object = m_model->objects[object_idx]; - if (model_object != nullptr) { - if (selection_mode == Selection::Instance) { -#if ENABLE_WORLD_COORDINATE - model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); -#else - model_object->instances[instance_idx]->set_rotation(v->get_instance_rotation()); - model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); -#endif // ENABLE_WORLD_COORDINATE - } - else if (selection_mode == Selection::Volume) { -#if ENABLE_WORLD_COORDINATE - model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); -#else - model_object->volumes[volume_idx]->set_rotation(v->get_volume_rotation()); - model_object->volumes[volume_idx]->set_offset(v->get_volume_offset()); -#endif // ENABLE_WORLD_COORDINATE - } - model_object->invalidate_bounding_box(); - } - } - - // Fixes sinking/flying instances - for (const std::pair& i : done) { - ModelObject* m = m_model->objects[i.first]; - const double shift_z = m->get_instance_min_z(i.second); - // leave sinking instances as sinking - if (min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) { - const Vec3d shift(0.0, 0.0, -shift_z); - m_selection.translate(i.first, i.second, shift); - m->translate_instance(i.second, shift); - } - - wxGetApp().obj_list()->update_info_items(static_cast(i.first)); - } - - if (!done.empty()) - post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED)); - - m_dirty = true; -} - -void GLCanvas3D::do_scale(const std::string& snapshot_type) -{ - if (m_model == nullptr) - return; - - if (!snapshot_type.empty()) - wxGetApp().plater()->take_snapshot(_(snapshot_type)); - - // stores current min_z of instances - std::map, double> min_zs; - if (!snapshot_type.empty()) { - for (int i = 0; i < static_cast(m_model->objects.size()); ++i) { - const ModelObject* obj = m_model->objects[i]; - for (int j = 0; j < static_cast(obj->instances.size()); ++j) { - min_zs[{ i, j }] = obj->instance_bounding_box(j).min.z(); - } - } - } - - std::set> done; // keeps track of modified instances - - Selection::EMode selection_mode = m_selection.get_mode(); - - for (const GLVolume* v : m_volumes.volumes) { - const int object_idx = v->object_idx(); - if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) - continue; - - const int instance_idx = v->instance_idx(); - const int volume_idx = v->volume_idx(); - - done.insert(std::pair(object_idx, instance_idx)); - - // Rotate instances/volumes - ModelObject* model_object = m_model->objects[object_idx]; - if (model_object != nullptr) { - if (selection_mode == Selection::Instance) { -#if ENABLE_WORLD_COORDINATE - model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); -#else - model_object->instances[instance_idx]->set_scaling_factor(v->get_instance_scaling_factor()); - model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); -#endif // ENABLE_WORLD_COORDINATE - } - else if (selection_mode == Selection::Volume) { -#if ENABLE_WORLD_COORDINATE - model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); - model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); -#else - model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); - model_object->volumes[volume_idx]->set_scaling_factor(v->get_volume_scaling_factor()); - model_object->volumes[volume_idx]->set_offset(v->get_volume_offset()); -#endif // ENABLE_WORLD_COORDINATE - } - model_object->invalidate_bounding_box(); - } - } - - // Fixes sinking/flying instances - for (const std::pair& i : done) { - ModelObject* m = m_model->objects[i.first]; - const double shift_z = m->get_instance_min_z(i.second); - // leave sinking instances as sinking - if (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) { - const Vec3d shift(0.0, 0.0, -shift_z); - m_selection.translate(i.first, i.second, shift); - m->translate_instance(i.second, shift); - } - wxGetApp().obj_list()->update_info_items(static_cast(i.first)); - } - - if (!done.empty()) - post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED)); - - m_dirty = true; -} - -void GLCanvas3D::do_mirror(const std::string& snapshot_type) -{ - if (m_model == nullptr) - return; - - if (!snapshot_type.empty()) - wxGetApp().plater()->take_snapshot(_(snapshot_type)); - - // stores current min_z of instances - std::map, double> min_zs; - if (!snapshot_type.empty()) { - for (int i = 0; i < static_cast(m_model->objects.size()); ++i) { - const ModelObject* obj = m_model->objects[i]; - for (int j = 0; j < static_cast(obj->instances.size()); ++j) { - min_zs[{ i, j }] = obj->instance_bounding_box(j).min.z(); - } - } - } - - std::set> done; // keeps track of modified instances - - Selection::EMode selection_mode = m_selection.get_mode(); - - for (const GLVolume* v : m_volumes.volumes) { - int object_idx = v->object_idx(); - if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) - continue; - - int instance_idx = v->instance_idx(); - int volume_idx = v->volume_idx(); - - done.insert(std::pair(object_idx, instance_idx)); - - // Mirror instances/volumes - ModelObject* model_object = m_model->objects[object_idx]; - if (model_object != nullptr) { - if (selection_mode == Selection::Instance) -#if ENABLE_WORLD_COORDINATE - model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); -#else - model_object->instances[instance_idx]->set_mirror(v->get_instance_mirror()); -#endif // ENABLE_WORLD_COORDINATE - else if (selection_mode == Selection::Volume) -#if ENABLE_WORLD_COORDINATE - model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); -#else - model_object->volumes[volume_idx]->set_mirror(v->get_volume_mirror()); -#endif // ENABLE_WORLD_COORDINATE - - model_object->invalidate_bounding_box(); - } - } - - // Fixes sinking/flying instances - for (const std::pair& i : done) { - ModelObject* m = m_model->objects[i.first]; - double shift_z = m->get_instance_min_z(i.second); - // leave sinking instances as sinking - if (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) { - Vec3d shift(0.0, 0.0, -shift_z); - m_selection.translate(i.first, i.second, shift); - m->translate_instance(i.second, shift); - } - wxGetApp().obj_list()->update_info_items(static_cast(i.first)); - } - - post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - - m_dirty = true; -} - -#if ENABLE_WORLD_COORDINATE -void GLCanvas3D::do_reset_skew(const std::string& snapshot_type) -{ - if (m_model == nullptr) - return; - - if (!snapshot_type.empty()) - wxGetApp().plater()->take_snapshot(_(snapshot_type)); - - std::set> done; // keeps track of modified instances - - const Selection::IndicesList& idxs = m_selection.get_volume_idxs(); - - for (unsigned int id : idxs) { - const GLVolume* v = m_volumes.volumes[id]; - int object_idx = v->object_idx(); - if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) - continue; - - int instance_idx = v->instance_idx(); - int volume_idx = v->volume_idx(); - - done.insert(std::pair(object_idx, instance_idx)); - - ModelObject* model_object = m_model->objects[object_idx]; - if (model_object != nullptr) { - model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); - model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); - model_object->invalidate_bounding_box(); - } - } - - post_event(SimpleEvent(EVT_GLCANVAS_RESET_SKEW)); - - m_dirty = true; -} -#endif // ENABLE_WORLD_COORDINATE - -void GLCanvas3D::update_gizmos_on_off_state() -{ - set_as_dirty(); - m_gizmos.update_data(); - m_gizmos.refresh_on_off_state(); -} - -void GLCanvas3D::handle_sidebar_focus_event(const std::string& opt_key, bool focus_on) -{ - m_sidebar_field = focus_on ? opt_key : ""; - if (!m_sidebar_field.empty()) - m_gizmos.reset_all_states(); - - m_dirty = true; -} - -void GLCanvas3D::handle_layers_data_focus_event(const t_layer_height_range range, const EditorType type) -{ - std::string field = "layer_" + std::to_string(type) + "_" + float_to_string_decimal_point(range.first) + "_" + float_to_string_decimal_point(range.second); - handle_sidebar_focus_event(field, true); -} - -void GLCanvas3D::update_ui_from_settings() -{ - m_dirty = true; - -#if __APPLE__ - // Update OpenGL scaling on OSX after the user toggled the "use_retina_opengl" settings in Preferences dialog. - const float orig_scaling = m_retina_helper->get_scale_factor(); - - const bool use_retina = wxGetApp().app_config->get("use_retina_opengl") == "1"; - BOOST_LOG_TRIVIAL(debug) << "GLCanvas3D: Use Retina OpenGL: " << use_retina; - m_retina_helper->set_use_retina(use_retina); - const float new_scaling = m_retina_helper->get_scale_factor(); - - if (new_scaling != orig_scaling) { - BOOST_LOG_TRIVIAL(debug) << "GLCanvas3D: Scaling factor: " << new_scaling; - - Camera& camera = wxGetApp().plater()->get_camera(); - camera.set_zoom(camera.get_zoom() * new_scaling / orig_scaling); - _refresh_if_shown_on_screen(); - } -#endif // ENABLE_RETINA_GL - - if (wxGetApp().is_editor()) - wxGetApp().plater()->enable_collapse_toolbar(wxGetApp().app_config->get("show_collapse_button") == "1"); -} - -GLCanvas3D::WipeTowerInfo GLCanvas3D::get_wipe_tower_info() const -{ - WipeTowerInfo wti; - - for (const GLVolume* vol : m_volumes.volumes) { - if (vol->is_wipe_tower) { - wti.m_pos = Vec2d(m_config->opt_float("wipe_tower_x"), - m_config->opt_float("wipe_tower_y")); - wti.m_rotation = (M_PI/180.) * m_config->opt_float("wipe_tower_rotation_angle"); - const BoundingBoxf3& bb = vol->bounding_box(); - wti.m_bb = BoundingBoxf{to_2d(bb.min), to_2d(bb.max)}; - break; - } - } - - return wti; -} - -Linef3 GLCanvas3D::mouse_ray(const Point& mouse_pos) -{ - float z0 = 0.0f; - float z1 = 1.0f; - return Linef3(_mouse_to_3d(mouse_pos, &z0), _mouse_to_3d(mouse_pos, &z1)); -} - -double GLCanvas3D::get_size_proportional_to_max_bed_size(double factor) const -{ - const BoundingBoxf& bbox = m_bed.build_volume().bounding_volume2d(); - return factor * std::max(bbox.size()[0], bbox.size()[1]); -} - -void GLCanvas3D::set_cursor(ECursorType type) -{ - if ((m_canvas != nullptr) && (m_cursor_type != type)) - { - switch (type) - { - case Standard: { m_canvas->SetCursor(*wxSTANDARD_CURSOR); break; } - case Cross: { m_canvas->SetCursor(*wxCROSS_CURSOR); break; } - } - - m_cursor_type = type; - } -} - -void GLCanvas3D::msw_rescale() -{ - m_gcode_viewer.invalidate_legend(); -} - -void GLCanvas3D::update_tooltip_for_settings_item_in_main_toolbar() -{ - std::string new_tooltip = _u8L("Switch to Settings") + - "\n" + "[" + GUI::shortkey_ctrl_prefix() + "2] - " + _u8L("Print Settings Tab") + - "\n" + "[" + GUI::shortkey_ctrl_prefix() + "3] - " + (current_printer_technology() == ptFFF ? _u8L("Filament Settings Tab") : _u8L("Material Settings Tab")) + - "\n" + "[" + GUI::shortkey_ctrl_prefix() + "4] - " + _u8L("Printer Settings Tab") ; - - m_main_toolbar.set_tooltip(get_main_toolbar_item_id("settings"), new_tooltip); -} - -bool GLCanvas3D::has_toolpaths_to_export() const -{ - return m_gcode_viewer.can_export_toolpaths(); -} - -void GLCanvas3D::export_toolpaths_to_obj(const char* filename) const -{ - m_gcode_viewer.export_toolpaths_to_obj(filename); -} - -void GLCanvas3D::mouse_up_cleanup() -{ - m_moving = false; - m_mouse.drag.move_volume_idx = -1; - m_mouse.set_start_position_3D_as_invalid(); - m_mouse.set_start_position_2D_as_invalid(); - m_mouse.dragging = false; - m_mouse.ignore_left_up = false; - m_dirty = true; - - if (m_canvas->HasCapture()) - m_canvas->ReleaseMouse(); -} - -void GLCanvas3D::update_sequential_clearance() -{ - if (current_printer_technology() != ptFFF || !fff_print()->config().complete_objects) - return; - - if (m_layers_editing.is_enabled() || m_gizmos.is_dragging()) - return; - - // collects instance transformations from volumes - // first define temporary cache - unsigned int instances_count = 0; - std::vector>> instance_transforms; - for (size_t obj = 0; obj < m_model->objects.size(); ++obj) { - instance_transforms.emplace_back(std::vector>()); - const ModelObject* model_object = m_model->objects[obj]; - for (size_t i = 0; i < model_object->instances.size(); ++i) { - instance_transforms[obj].emplace_back(std::optional()); - ++instances_count; - } - } - - if (instances_count == 1) - return; - - // second fill temporary cache with data from volumes - for (const GLVolume* v : m_volumes.volumes) { - if (v->is_modifier || v->is_wipe_tower) - continue; - - auto& transform = instance_transforms[v->object_idx()][v->instance_idx()]; - if (!transform.has_value()) - transform = v->get_instance_transformation(); - } - - // calculates objects 2d hulls (see also: Print::sequential_print_horizontal_clearance_valid()) - // this is done only the first time this method is called while moving the mouse, - // the results are then cached for following displacements - if (m_sequential_print_clearance_first_displacement) { - m_sequential_print_clearance.m_hull_2d_cache.clear(); - float shrink_factor = static_cast(scale_(0.5 * fff_print()->config().extruder_clearance_radius.value - EPSILON)); - double mitter_limit = scale_(0.1); - m_sequential_print_clearance.m_hull_2d_cache.reserve(m_model->objects.size()); - for (size_t i = 0; i < m_model->objects.size(); ++i) { - ModelObject* model_object = m_model->objects[i]; - ModelInstance* model_instance0 = model_object->instances.front(); - Polygon hull_2d = offset(model_object->convex_hull_2d(Geometry::assemble_transform({ 0.0, 0.0, model_instance0->get_offset().z() }, model_instance0->get_rotation(), - model_instance0->get_scaling_factor(), model_instance0->get_mirror())), - // Shrink the extruder_clearance_radius a tiny bit, so that if the object arrangement algorithm placed the objects - // exactly by satisfying the extruder_clearance_radius, this test will not trigger collision. - shrink_factor, - jtRound, mitter_limit).front(); - - Pointf3s& cache_hull_2d = m_sequential_print_clearance.m_hull_2d_cache.emplace_back(Pointf3s()); - cache_hull_2d.reserve(hull_2d.points.size()); - for (const Point& p : hull_2d.points) { - cache_hull_2d.emplace_back(unscale(p.x()), unscale(p.y()), 0.0); - } - } - m_sequential_print_clearance_first_displacement = false; - } - - // calculates instances 2d hulls (see also: Print::sequential_print_horizontal_clearance_valid()) - Polygons polygons; - polygons.reserve(instances_count); - for (size_t i = 0; i < instance_transforms.size(); ++i) { - const auto& instances = instance_transforms[i]; - double rotation_z0 = instances.front()->get_rotation().z(); - for (const auto& instance : instances) { - Geometry::Transformation transformation; - const Vec3d& offset = instance->get_offset(); - transformation.set_offset({ offset.x(), offset.y(), 0.0 }); - transformation.set_rotation(Z, instance->get_rotation().z() - rotation_z0); - const Transform3d& trafo = transformation.get_matrix(); - const Pointf3s& hull_2d = m_sequential_print_clearance.m_hull_2d_cache[i]; - Points inst_pts; - inst_pts.reserve(hull_2d.size()); - for (size_t j = 0; j < hull_2d.size(); ++j) { - const Vec3d p = trafo * hull_2d[j]; - inst_pts.emplace_back(scaled(p.x()), scaled(p.y())); - } - polygons.emplace_back(Geometry::convex_hull(std::move(inst_pts))); - } - } - - // sends instances 2d hulls to be rendered - set_sequential_print_clearance_visible(true); - set_sequential_print_clearance_render_fill(false); - set_sequential_print_clearance_polygons(polygons); -} - -bool GLCanvas3D::is_object_sinking(int object_idx) const -{ - for (const GLVolume* v : m_volumes.volumes) { - if (v->object_idx() == object_idx && (v->is_sinking() || (!v->is_modifier && v->is_below_printbed()))) - return true; - } - return false; -} - -bool GLCanvas3D::_is_shown_on_screen() const -{ - return (m_canvas != nullptr) ? m_canvas->IsShownOnScreen() : false; -} - -// Getter for the const char*[] -static bool string_getter(const bool is_undo, int idx, const char** out_text) -{ - return wxGetApp().plater()->undo_redo_string_getter(is_undo, idx, out_text); -} - -bool GLCanvas3D::_render_undo_redo_stack(const bool is_undo, float pos_x) -{ - bool action_taken = false; - - ImGuiWrapper* imgui = wxGetApp().imgui(); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - imgui->set_next_window_pos(pos_x, m_undoredo_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); -#else - const float x = pos_x * (float)wxGetApp().plater()->get_camera().get_zoom() + 0.5f * (float)get_canvas_size().get_width(); - imgui->set_next_window_pos(x, m_undoredo_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - std::string title = is_undo ? L("Undo History") : L("Redo History"); - imgui->begin(_(title), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - - int hovered = m_imgui_undo_redo_hovered_pos; - int selected = -1; - float em = static_cast(wxGetApp().em_unit()); -#if ENABLE_RETINA_GL - em *= m_retina_helper->get_scale_factor(); -#endif - - if (imgui->undo_redo_list(ImVec2(18 * em, 26 * em), is_undo, &string_getter, hovered, selected, m_mouse_wheel)) - m_imgui_undo_redo_hovered_pos = hovered; - else - m_imgui_undo_redo_hovered_pos = -1; - - if (selected >= 0) { - is_undo ? wxGetApp().plater()->undo_to(selected) : wxGetApp().plater()->redo_to(selected); - action_taken = true; - } - - imgui->text(wxString::Format(is_undo ? _L_PLURAL("Undo %1$d Action", "Undo %1$d Actions", hovered + 1) : _L_PLURAL("Redo %1$d Action", "Redo %1$d Actions", hovered + 1), hovered + 1)); - - imgui->end(); - - return action_taken; -} - -// Getter for the const char*[] for the search list -static bool search_string_getter(int idx, const char** label, const char** tooltip) -{ - return wxGetApp().plater()->search_string_getter(idx, label, tooltip); -} - -bool GLCanvas3D::_render_search_list(float pos_x) -{ - bool action_taken = false; - ImGuiWrapper* imgui = wxGetApp().imgui(); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - imgui->set_next_window_pos(pos_x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); -#else - const float x = /*pos_x * (float)wxGetApp().plater()->get_camera().get_zoom() + */0.5f * (float)get_canvas_size().get_width(); - imgui->set_next_window_pos(x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - std::string title = L("Search"); - imgui->begin(_(title), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - - int selected = -1; - bool edited = false; - float em = static_cast(wxGetApp().em_unit()); -#if ENABLE_RETINA_GL - em *= m_retina_helper->get_scale_factor(); -#endif // ENABLE_RETINA_GL - - Sidebar& sidebar = wxGetApp().sidebar(); - - std::string& search_line = sidebar.get_search_line(); - char *s = new char[255]; - strcpy(s, search_line.empty() ? _u8L("Enter a search term").c_str() : search_line.c_str()); - - imgui->search_list(ImVec2(45 * em, 30 * em), &search_string_getter, s, - sidebar.get_searcher().view_params, - selected, edited, m_mouse_wheel, wxGetApp().is_localized()); - - search_line = s; - delete [] s; - if (search_line == _u8L("Enter a search term")) - search_line.clear(); - - if (edited) - sidebar.search(); - - if (selected >= 0) { - // selected == 9999 means that Esc kye was pressed - /*// revert commit https://github.com/prusa3d/PrusaSlicer/commit/91897589928789b261ca0dc735ffd46f2b0b99f2 - if (selected == 9999) - action_taken = true; - else - sidebar.jump_to_option(selected);*/ - if (selected != 9999) { - imgui->end(); // end imgui before the jump to option - sidebar.jump_to_option(selected); - return true; - } - action_taken = true; - } - - imgui->end(); - - return action_taken; -} - -bool GLCanvas3D::_render_arrange_menu(float pos_x) -{ - ImGuiWrapper *imgui = wxGetApp().imgui(); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - imgui->set_next_window_pos(pos_x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); -#else - auto canvas_w = float(get_canvas_size().get_width()); - const float x = pos_x * float(wxGetApp().plater()->get_camera().get_zoom()) + 0.5f * canvas_w; - imgui->set_next_window_pos(x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - imgui->begin(_L("Arrange options"), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); - - ArrangeSettings settings = get_arrange_settings(); - ArrangeSettings &settings_out = get_arrange_settings(); - - auto &appcfg = wxGetApp().app_config; - PrinterTechnology ptech = current_printer_technology(); - - bool settings_changed = false; - float dist_min = 0.f; - float dist_bed_min = 0.f; - std::string dist_key = "min_object_distance"; - std::string dist_bed_key = "min_bed_distance"; - std::string rot_key = "enable_rotation"; - std::string postfix; - - if (ptech == ptSLA) { - postfix = "_sla"; - } else if (ptech == ptFFF) { - auto co_opt = m_config->option("complete_objects"); - if (co_opt && co_opt->value) { - dist_min = float(min_object_distance(*m_config)); - postfix = "_fff_seq_print"; - } else { - dist_min = 0.f; - postfix = "_fff"; - } - } - - dist_key += postfix; - dist_bed_key += postfix; - rot_key += postfix; - - imgui->text(GUI::format_wxstr(_L("Press %1%left mouse button to enter the exact value"), shortkey_ctrl_prefix())); - - if (imgui->slider_float(_L("Spacing"), &settings.distance, dist_min, 100.0f, "%5.2f") || dist_min > settings.distance) { - settings.distance = std::max(dist_min, settings.distance); - settings_out.distance = settings.distance; - appcfg->set("arrange", dist_key.c_str(), float_to_string_decimal_point(settings_out.distance)); - settings_changed = true; - } - - if (imgui->slider_float(_L("Spacing from bed"), &settings.distance_from_bed, dist_bed_min, 100.0f, "%5.2f") || dist_bed_min > settings.distance_from_bed) { - settings.distance_from_bed = std::max(dist_bed_min, settings.distance_from_bed); - settings_out.distance_from_bed = settings.distance_from_bed; - appcfg->set("arrange", dist_bed_key.c_str(), float_to_string_decimal_point(settings_out.distance_from_bed)); - settings_changed = true; - } - - if (imgui->checkbox(_L("Enable rotations (slow)"), settings.enable_rotation)) { - settings_out.enable_rotation = settings.enable_rotation; - appcfg->set("arrange", rot_key.c_str(), settings_out.enable_rotation? "1" : "0"); - settings_changed = true; - } - - ImGui::Separator(); - - if (imgui->button(_L("Reset"))) { - settings_out = ArrangeSettings{}; - settings_out.distance = std::max(dist_min, settings_out.distance); - appcfg->set("arrange", dist_key.c_str(), float_to_string_decimal_point(settings_out.distance)); - appcfg->set("arrange", dist_bed_key.c_str(), float_to_string_decimal_point(settings_out.distance_from_bed)); - appcfg->set("arrange", rot_key.c_str(), settings_out.enable_rotation? "1" : "0"); - settings_changed = true; - } - - ImGui::SameLine(); - - if (imgui->button(_L("Arrange"))) { - wxGetApp().plater()->arrange(); - } - - imgui->end(); - - return settings_changed; -} - -#define ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT 0 -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT -static void debug_output_thumbnail(const ThumbnailData& thumbnail_data) -{ - // debug export of generated image - wxImage image(thumbnail_data.width, thumbnail_data.height); - image.InitAlpha(); - - for (unsigned int r = 0; r < thumbnail_data.height; ++r) - { - unsigned int rr = (thumbnail_data.height - 1 - r) * thumbnail_data.width; - for (unsigned int c = 0; c < thumbnail_data.width; ++c) - { - unsigned char* px = (unsigned char*)thumbnail_data.pixels.data() + 4 * (rr + c); - image.SetRGB((int)c, (int)r, px[0], px[1], px[2]); - image.SetAlpha((int)c, (int)r, px[3]); - } - } - - image.SaveFile("C:/prusa/test/test.png", wxBITMAP_TYPE_PNG); -} -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT - -void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type) -{ - auto is_visible = [](const GLVolume& v) { - bool ret = v.printable; - ret &= (!v.shader_outside_printer_detection_enabled || !v.is_outside); - return ret; - }; - - GLVolumePtrs visible_volumes; - - for (GLVolume* vol : volumes.volumes) { - if (!vol->is_modifier && !vol->is_wipe_tower && (!thumbnail_params.parts_only || vol->composite_id.volume_id >= 0)) { - if (!thumbnail_params.printable_only || is_visible(*vol)) - visible_volumes.emplace_back(vol); - } - } - - BoundingBoxf3 volumes_box; - if (!visible_volumes.empty()) { - for (const GLVolume* vol : visible_volumes) { - volumes_box.merge(vol->transformed_bounding_box()); - } - } - else - // This happens for empty projects - volumes_box = m_bed.extended_bounding_box(); - - Camera camera; - camera.set_type(camera_type); - camera.set_scene_box(scene_bounding_box()); -#if ENABLE_RAYCAST_PICKING - camera.set_viewport(0, 0, thumbnail_data.width, thumbnail_data.height); - camera.apply_viewport(); -#else - camera.apply_viewport(0, 0, thumbnail_data.width, thumbnail_data.height); -#endif // ENABLE_RAYCAST_PICKING - camera.zoom_to_box(volumes_box); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Transform3d& view_matrix = camera.get_view_matrix(); -#else - camera.apply_view_matrix(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - double near_z = -1.0; - double far_z = -1.0; - - if (thumbnail_params.show_bed) { - // extends the near and far z of the frustrum to avoid the bed being clipped - - // box in eye space -#if ENABLE_LEGACY_OPENGL_REMOVAL - const BoundingBoxf3 t_bed_box = m_bed.extended_bounding_box().transformed(view_matrix); -#else - const BoundingBoxf3 t_bed_box = m_bed.extended_bounding_box().transformed(camera.get_view_matrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - near_z = -t_bed_box.max.z(); - far_z = -t_bed_box.min.z(); - } - - camera.apply_projection(volumes_box, near_z, far_z); - - GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); - if (shader == nullptr) - return; - - if (thumbnail_params.transparent_background) - glsafe(::glClearColor(0.0f, 0.0f, 0.0f, 0.0f)); - - glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); - glsafe(::glEnable(GL_DEPTH_TEST)); - - shader->start_using(); - shader->set_uniform("emission_factor", 0.0f); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Transform3d& projection_matrix = camera.get_projection_matrix(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - for (GLVolume* vol : visible_volumes) { -#if ENABLE_LEGACY_OPENGL_REMOVAL - vol->model.set_color((vol->printable && !vol->is_outside) ? (current_printer_technology() == ptSLA ? vol->color : ColorRGBA::ORANGE()) : ColorRGBA::GRAY()); -#else - shader->set_uniform("uniform_color", (vol->printable && !vol->is_outside) ? (current_printer_technology() == ptSLA ? vol->color : ColorRGBA::ORANGE()) : ColorRGBA::GRAY()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - // the volume may have been deactivated by an active gizmo - const bool is_active = vol->is_active; - vol->is_active = true; -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Transform3d model_matrix = vol->world_matrix(); - shader->set_uniform("view_model_matrix", view_matrix * model_matrix); - shader->set_uniform("projection_matrix", projection_matrix); - const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); - shader->set_uniform("view_normal_matrix", view_normal_matrix); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - vol->render(); - vol->is_active = is_active; - } - - shader->stop_using(); - - glsafe(::glDisable(GL_DEPTH_TEST)); - - if (thumbnail_params.show_bed) -#if ENABLE_LEGACY_OPENGL_REMOVAL - _render_bed(view_matrix, projection_matrix, !camera.is_looking_downward(), false); -#else - _render_bed(!camera.is_looking_downward(), false); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - // restore background color - if (thumbnail_params.transparent_background) - glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 1.0f)); -} - -void GLCanvas3D::_render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type) -{ - thumbnail_data.set(w, h); - if (!thumbnail_data.is_valid()) - return; - - bool multisample = m_multisample_allowed; - if (multisample) - glsafe(::glEnable(GL_MULTISAMPLE)); - - GLint max_samples; - glsafe(::glGetIntegerv(GL_MAX_SAMPLES, &max_samples)); - GLsizei num_samples = max_samples / 2; - - GLuint render_fbo; - glsafe(::glGenFramebuffers(1, &render_fbo)); - glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, render_fbo)); - - GLuint render_tex = 0; - GLuint render_tex_buffer = 0; - if (multisample) { - // use renderbuffer instead of texture to avoid the need to use glTexImage2DMultisample which is available only since OpenGL 3.2 - glsafe(::glGenRenderbuffers(1, &render_tex_buffer)); - glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, render_tex_buffer)); - glsafe(::glRenderbufferStorageMultisample(GL_RENDERBUFFER, num_samples, GL_RGBA8, w, h)); - glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, render_tex_buffer)); - } - else { - glsafe(::glGenTextures(1, &render_tex)); - glsafe(::glBindTexture(GL_TEXTURE_2D, render_tex)); - glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); - glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, render_tex, 0)); - } - - GLuint render_depth; - glsafe(::glGenRenderbuffers(1, &render_depth)); - glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, render_depth)); - if (multisample) - glsafe(::glRenderbufferStorageMultisample(GL_RENDERBUFFER, num_samples, GL_DEPTH_COMPONENT24, w, h)); - else - glsafe(::glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, w, h)); - - glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, render_depth)); - - GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 }; - glsafe(::glDrawBuffers(1, drawBufs)); - - if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { - _render_thumbnail_internal(thumbnail_data, thumbnail_params, volumes, camera_type); - - if (multisample) { - GLuint resolve_fbo; - glsafe(::glGenFramebuffers(1, &resolve_fbo)); - glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, resolve_fbo)); - - GLuint resolve_tex; - glsafe(::glGenTextures(1, &resolve_tex)); - glsafe(::glBindTexture(GL_TEXTURE_2D, resolve_tex)); - glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); - glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, resolve_tex, 0)); - - glsafe(::glDrawBuffers(1, drawBufs)); - - if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { - glsafe(::glBindFramebuffer(GL_READ_FRAMEBUFFER, render_fbo)); - glsafe(::glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolve_fbo)); - glsafe(::glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_LINEAR)); - - glsafe(::glBindFramebuffer(GL_READ_FRAMEBUFFER, resolve_fbo)); - glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data())); - } - - glsafe(::glDeleteTextures(1, &resolve_tex)); - glsafe(::glDeleteFramebuffers(1, &resolve_fbo)); - } - else - glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data())); - -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT - debug_output_thumbnail(thumbnail_data); -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT - } - - glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, 0)); - glsafe(::glDeleteRenderbuffers(1, &render_depth)); - if (render_tex_buffer != 0) - glsafe(::glDeleteRenderbuffers(1, &render_tex_buffer)); - if (render_tex != 0) - glsafe(::glDeleteTextures(1, &render_tex)); - glsafe(::glDeleteFramebuffers(1, &render_fbo)); - - if (multisample) - glsafe(::glDisable(GL_MULTISAMPLE)); -} - -void GLCanvas3D::_render_thumbnail_framebuffer_ext(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type) -{ - thumbnail_data.set(w, h); - if (!thumbnail_data.is_valid()) - return; - - bool multisample = m_multisample_allowed; - if (multisample) - glsafe(::glEnable(GL_MULTISAMPLE)); - - GLint max_samples; - glsafe(::glGetIntegerv(GL_MAX_SAMPLES_EXT, &max_samples)); - GLsizei num_samples = max_samples / 2; - - GLuint render_fbo; - glsafe(::glGenFramebuffersEXT(1, &render_fbo)); - glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, render_fbo)); - - GLuint render_tex = 0; - GLuint render_tex_buffer = 0; - if (multisample) { - // use renderbuffer instead of texture to avoid the need to use glTexImage2DMultisample which is available only since OpenGL 3.2 - glsafe(::glGenRenderbuffersEXT(1, &render_tex_buffer)); - glsafe(::glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, render_tex_buffer)); - glsafe(::glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER_EXT, num_samples, GL_RGBA8, w, h)); - glsafe(::glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_RENDERBUFFER_EXT, render_tex_buffer)); - } - else { - glsafe(::glGenTextures(1, &render_tex)); - glsafe(::glBindTexture(GL_TEXTURE_2D, render_tex)); - glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); - glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, render_tex, 0)); - } - - GLuint render_depth; - glsafe(::glGenRenderbuffersEXT(1, &render_depth)); - glsafe(::glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, render_depth)); - if (multisample) - glsafe(::glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER_EXT, num_samples, GL_DEPTH_COMPONENT24, w, h)); - else - glsafe(::glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, w, h)); - - glsafe(::glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, render_depth)); - - GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 }; - glsafe(::glDrawBuffers(1, drawBufs)); - - if (::glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) == GL_FRAMEBUFFER_COMPLETE_EXT) { - _render_thumbnail_internal(thumbnail_data, thumbnail_params, volumes, camera_type); - - if (multisample) { - GLuint resolve_fbo; - glsafe(::glGenFramebuffersEXT(1, &resolve_fbo)); - glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, resolve_fbo)); - - GLuint resolve_tex; - glsafe(::glGenTextures(1, &resolve_tex)); - glsafe(::glBindTexture(GL_TEXTURE_2D, resolve_tex)); - glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); - glsafe(::glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, resolve_tex, 0)); - - glsafe(::glDrawBuffers(1, drawBufs)); - - if (::glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) == GL_FRAMEBUFFER_COMPLETE_EXT) { - glsafe(::glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, render_fbo)); - glsafe(::glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, resolve_fbo)); - glsafe(::glBlitFramebufferEXT(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_LINEAR)); - - glsafe(::glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, resolve_fbo)); - glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data())); - } - - glsafe(::glDeleteTextures(1, &resolve_tex)); - glsafe(::glDeleteFramebuffersEXT(1, &resolve_fbo)); - } - else - glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data())); - -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT - debug_output_thumbnail(thumbnail_data); -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT - } - - glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0)); - glsafe(::glDeleteRenderbuffersEXT(1, &render_depth)); - if (render_tex_buffer != 0) - glsafe(::glDeleteRenderbuffersEXT(1, &render_tex_buffer)); - if (render_tex != 0) - glsafe(::glDeleteTextures(1, &render_tex)); - glsafe(::glDeleteFramebuffersEXT(1, &render_fbo)); - - if (multisample) - glsafe(::glDisable(GL_MULTISAMPLE)); -} - -void GLCanvas3D::_render_thumbnail_legacy(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type) -{ - // check that thumbnail size does not exceed the default framebuffer size - const Size& cnv_size = get_canvas_size(); - unsigned int cnv_w = (unsigned int)cnv_size.get_width(); - unsigned int cnv_h = (unsigned int)cnv_size.get_height(); - if (w > cnv_w || h > cnv_h) { - float ratio = std::min((float)cnv_w / (float)w, (float)cnv_h / (float)h); - w = (unsigned int)(ratio * (float)w); - h = (unsigned int)(ratio * (float)h); - } - - thumbnail_data.set(w, h); - if (!thumbnail_data.is_valid()) - return; - - _render_thumbnail_internal(thumbnail_data, thumbnail_params, volumes, camera_type); - - glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data())); -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT - debug_output_thumbnail(thumbnail_data); -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT - - // restore the default framebuffer size to avoid flickering on the 3D scene -#if ENABLE_RAYCAST_PICKING - wxGetApp().plater()->get_camera().apply_viewport(); -#else - wxGetApp().plater()->get_camera().apply_viewport(0, 0, cnv_size.get_width(), cnv_size.get_height()); -#endif // ENABLE_RAYCAST_PICKING -} - -bool GLCanvas3D::_init_toolbars() -{ - if (!_init_main_toolbar()) - return false; - - if (!_init_undoredo_toolbar()) - return false; - - if (!_init_view_toolbar()) - return false; - - if (!_init_collapse_toolbar()) - return false; - - return true; -} - -bool GLCanvas3D::_init_main_toolbar() -{ - if (!m_main_toolbar.is_enabled()) - return true; - - BackgroundTexture::Metadata background_data; - background_data.filename = "toolbar_background.png"; - background_data.left = 16; - background_data.top = 16; - background_data.right = 16; - background_data.bottom = 16; - - if (!m_main_toolbar.init(background_data)) { - // unable to init the toolbar texture, disable it - m_main_toolbar.set_enabled(false); - return true; - } - // init arrow -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_main_toolbar.init_arrow("toolbar_arrow_2.svg")) -#else - BackgroundTexture::Metadata arrow_data; - arrow_data.filename = "toolbar_arrow.svg"; - arrow_data.left = 0; - arrow_data.top = 0; - arrow_data.right = 0; - arrow_data.bottom = 0; - if (!m_main_toolbar.init_arrow(arrow_data)) -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - BOOST_LOG_TRIVIAL(error) << "Main toolbar failed to load arrow texture."; - - // m_gizmos is created at constructor, thus we can init arrow here. -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_gizmos.init_arrow("toolbar_arrow_2.svg")) -#else - if (!m_gizmos.init_arrow(arrow_data)) -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - BOOST_LOG_TRIVIAL(error) << "Gizmos manager failed to load arrow texture."; - -// m_main_toolbar.set_layout_type(GLToolbar::Layout::Vertical); - m_main_toolbar.set_layout_type(GLToolbar::Layout::Horizontal); - m_main_toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Right); - m_main_toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Top); - m_main_toolbar.set_border(5.0f); - m_main_toolbar.set_separator_size(5); - m_main_toolbar.set_gap_size(4); - - GLToolbarItem::Data item; - - item.name = "add"; - item.icon_filename = "add.svg"; - item.tooltip = _utf8(L("Add...")) + " [" + GUI::shortkey_ctrl_prefix() + "I]"; - item.sprite_id = 0; - item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_ADD)); }; - if (!m_main_toolbar.add_item(item)) - return false; - - item.name = "delete"; - item.icon_filename = "remove.svg"; - item.tooltip = _utf8(L("Delete")) + " [Del]"; - item.sprite_id = 1; - item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_DELETE)); }; - item.enabling_callback = []()->bool { return wxGetApp().plater()->can_delete(); }; - if (!m_main_toolbar.add_item(item)) - return false; - - item.name = "deleteall"; - item.icon_filename = "delete_all.svg"; - item.tooltip = _utf8(L("Delete all")) + " [" + GUI::shortkey_ctrl_prefix() + "Del]"; - item.sprite_id = 2; - item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_DELETE_ALL)); }; - item.enabling_callback = []()->bool { return wxGetApp().plater()->can_delete_all(); }; - if (!m_main_toolbar.add_item(item)) - return false; - - item.name = "arrange"; - item.icon_filename = "arrange.svg"; - item.tooltip = _utf8(L("Arrange")) + " [A]\n" + _utf8(L("Arrange selection")) + " [Shift+A]\n" + _utf8(L("Click right mouse button to show arrangement options")); - item.sprite_id = 3; - item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_ARRANGE)); }; - item.enabling_callback = []()->bool { return wxGetApp().plater()->can_arrange(); }; - item.right.toggable = true; - item.right.render_callback = [this](float left, float right, float, float) { - if (m_canvas != nullptr) - _render_arrange_menu(0.5f * (left + right)); - }; - if (!m_main_toolbar.add_item(item)) - return false; - - item.right.toggable = false; - item.right.render_callback = GLToolbarItem::Default_Render_Callback; - - if (!m_main_toolbar.add_separator()) - return false; - - item.name = "copy"; - item.icon_filename = "copy.svg"; - item.tooltip = _utf8(L("Copy")) + " [" + GUI::shortkey_ctrl_prefix() + "C]"; - item.sprite_id = 4; - item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_COPY)); }; - item.enabling_callback = []()->bool { return wxGetApp().plater()->can_copy_to_clipboard(); }; - if (!m_main_toolbar.add_item(item)) - return false; - - item.name = "paste"; - item.icon_filename = "paste.svg"; - item.tooltip = _utf8(L("Paste")) + " [" + GUI::shortkey_ctrl_prefix() + "V]"; - item.sprite_id = 5; - item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_PASTE)); }; - item.enabling_callback = []()->bool { return wxGetApp().plater()->can_paste_from_clipboard(); }; - if (!m_main_toolbar.add_item(item)) - return false; - - if (!m_main_toolbar.add_separator()) - return false; - - item.name = "more"; - item.icon_filename = "instance_add.svg"; - item.tooltip = _utf8(L("Add instance")) + " [+]"; - item.sprite_id = 6; - item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_MORE)); }; - item.visibility_callback = []()->bool { return wxGetApp().get_mode() != comSimple; }; - item.enabling_callback = []()->bool { return wxGetApp().plater()->can_increase_instances(); }; - - if (!m_main_toolbar.add_item(item)) - return false; - - item.name = "fewer"; - item.icon_filename = "instance_remove.svg"; - item.tooltip = _utf8(L("Remove instance")) + " [-]"; - item.sprite_id = 7; - item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_FEWER)); }; - item.visibility_callback = []()->bool { return wxGetApp().get_mode() != comSimple; }; - item.enabling_callback = []()->bool { return wxGetApp().plater()->can_decrease_instances(); }; - if (!m_main_toolbar.add_item(item)) - return false; - - if (!m_main_toolbar.add_separator()) - return false; - - item.name = "splitobjects"; - item.icon_filename = "split_objects.svg"; - item.tooltip = _utf8(L("Split to objects")); - item.sprite_id = 8; - item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_SPLIT_OBJECTS)); }; - item.visibility_callback = GLToolbarItem::Default_Visibility_Callback; - item.enabling_callback = []()->bool { return wxGetApp().plater()->can_split_to_objects(); }; - if (!m_main_toolbar.add_item(item)) - return false; - - item.name = "splitvolumes"; - item.icon_filename = "split_parts.svg"; - item.tooltip = _utf8(L("Split to parts")); - item.sprite_id = 9; - item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_SPLIT_VOLUMES)); }; - item.visibility_callback = []()->bool { return wxGetApp().get_mode() != comSimple; }; - item.enabling_callback = []()->bool { return wxGetApp().plater()->can_split_to_volumes(); }; - if (!m_main_toolbar.add_item(item)) - return false; - - if (!m_main_toolbar.add_separator()) - return false; - - item.name = "settings"; - item.icon_filename = "settings.svg"; - item.tooltip = _u8L("Switch to Settings") + "\n" + "[" + GUI::shortkey_ctrl_prefix() + "2] - " + _u8L("Print Settings Tab") + - "\n" + "[" + GUI::shortkey_ctrl_prefix() + "3] - " + (current_printer_technology() == ptFFF ? _u8L("Filament Settings Tab") : _u8L("Material Settings Tab")) + - "\n" + "[" + GUI::shortkey_ctrl_prefix() + "4] - " + _u8L("Printer Settings Tab") ; - item.sprite_id = 10; - item.enabling_callback = GLToolbarItem::Default_Enabling_Callback; - item.visibility_callback = []() { return (wxGetApp().app_config->get("new_settings_layout_mode") == "1" || - wxGetApp().app_config->get("dlg_settings_layout_mode") == "1"); }; - item.left.action_callback = []() { wxGetApp().mainframe->select_tab(); }; - if (!m_main_toolbar.add_item(item)) - return false; - - /* - if (!m_main_toolbar.add_separator()) - return false; - */ - - item.name = "search"; - item.icon_filename = "search_.svg"; - item.tooltip = _utf8(L("Search")) + " [" + GUI::shortkey_ctrl_prefix() + "F]"; - item.sprite_id = 11; - item.left.toggable = true; - item.left.render_callback = [this](float left, float right, float, float) { - if (m_canvas != nullptr) { - if (_render_search_list(0.5f * (left + right))) - _deactivate_search_toolbar_item(); - } - }; - item.left.action_callback = GLToolbarItem::Default_Action_Callback; - item.visibility_callback = GLToolbarItem::Default_Visibility_Callback; - item.enabling_callback = GLToolbarItem::Default_Enabling_Callback; - if (!m_main_toolbar.add_item(item)) - return false; - - if (!m_main_toolbar.add_separator()) - return false; - - item.name = "layersediting"; - item.icon_filename = "layers_white.svg"; - item.tooltip = _utf8(L("Variable layer height")); - item.sprite_id = 12; - item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_LAYERSEDITING)); }; - item.visibility_callback = [this]()->bool { - bool res = current_printer_technology() == ptFFF; - // turns off if changing printer technology - if (!res && m_main_toolbar.is_item_visible("layersediting") && m_main_toolbar.is_item_pressed("layersediting")) - force_main_toolbar_left_action(get_main_toolbar_item_id("layersediting")); - - return res; - }; - item.enabling_callback = []()->bool { return wxGetApp().plater()->can_layers_editing(); }; - item.left.render_callback = GLToolbarItem::Default_Render_Callback; - if (!m_main_toolbar.add_item(item)) - return false; - - return true; -} - -bool GLCanvas3D::_init_undoredo_toolbar() -{ - if (!m_undoredo_toolbar.is_enabled()) - return true; - - BackgroundTexture::Metadata background_data; - background_data.filename = "toolbar_background.png"; - background_data.left = 16; - background_data.top = 16; - background_data.right = 16; - background_data.bottom = 16; - - if (!m_undoredo_toolbar.init(background_data)) { - // unable to init the toolbar texture, disable it - m_undoredo_toolbar.set_enabled(false); - return true; - } - - // init arrow -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_undoredo_toolbar.init_arrow("toolbar_arrow_2.svg")) -#else - BackgroundTexture::Metadata arrow_data; - arrow_data.filename = "toolbar_arrow.svg"; - arrow_data.left = 0; - arrow_data.top = 0; - arrow_data.right = 0; - arrow_data.bottom = 0; - if (!m_undoredo_toolbar.init_arrow(arrow_data)) -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - BOOST_LOG_TRIVIAL(error) << "Undo/Redo toolbar failed to load arrow texture."; - -// m_undoredo_toolbar.set_layout_type(GLToolbar::Layout::Vertical); - m_undoredo_toolbar.set_layout_type(GLToolbar::Layout::Horizontal); - m_undoredo_toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Left); - m_undoredo_toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Top); - m_undoredo_toolbar.set_border(5.0f); - m_undoredo_toolbar.set_separator_size(5); - m_undoredo_toolbar.set_gap_size(4); - - GLToolbarItem::Data item; - - item.name = "undo"; - item.icon_filename = "undo_toolbar.svg"; - item.tooltip = _utf8(L("Undo")) + " [" + GUI::shortkey_ctrl_prefix() + "Z]\n" + _utf8(L("Click right mouse button to open/close History")); - item.sprite_id = 0; - item.left.action_callback = [this]() { post_event(SimpleEvent(EVT_GLCANVAS_UNDO)); }; - item.right.toggable = true; - item.right.action_callback = [this]() { m_imgui_undo_redo_hovered_pos = -1; }; - item.right.render_callback = [this](float left, float right, float, float) { - if (m_canvas != nullptr) { - if (_render_undo_redo_stack(true, 0.5f * (left + right))) - _deactivate_undo_redo_toolbar_items(); - } - }; - item.enabling_callback = [this]()->bool { - bool can_undo = wxGetApp().plater()->can_undo(); - int id = m_undoredo_toolbar.get_item_id("undo"); - - std::string curr_additional_tooltip; - m_undoredo_toolbar.get_additional_tooltip(id, curr_additional_tooltip); - - std::string new_additional_tooltip; - if (can_undo) { - std::string action; - wxGetApp().plater()->undo_redo_topmost_string_getter(true, action); - new_additional_tooltip = (boost::format(_utf8(L("Next Undo action: %1%"))) % action).str(); - } - - if (new_additional_tooltip != curr_additional_tooltip) { - m_undoredo_toolbar.set_additional_tooltip(id, new_additional_tooltip); - set_tooltip(""); - } - return can_undo; - }; - - if (!m_undoredo_toolbar.add_item(item)) - return false; - - item.name = "redo"; - item.icon_filename = "redo_toolbar.svg"; - item.tooltip = _utf8(L("Redo")) + " [" + GUI::shortkey_ctrl_prefix() + "Y]\n" + _utf8(L("Click right mouse button to open/close History")); - item.sprite_id = 1; - item.left.action_callback = [this]() { post_event(SimpleEvent(EVT_GLCANVAS_REDO)); }; - item.right.action_callback = [this]() { m_imgui_undo_redo_hovered_pos = -1; }; - item.right.render_callback = [this](float left, float right, float, float) { - if (m_canvas != nullptr) { - if (_render_undo_redo_stack(false, 0.5f * (left + right))) - _deactivate_undo_redo_toolbar_items(); - } - }; - item.enabling_callback = [this]()->bool { - bool can_redo = wxGetApp().plater()->can_redo(); - int id = m_undoredo_toolbar.get_item_id("redo"); - - std::string curr_additional_tooltip; - m_undoredo_toolbar.get_additional_tooltip(id, curr_additional_tooltip); - - std::string new_additional_tooltip; - if (can_redo) { - std::string action; - wxGetApp().plater()->undo_redo_topmost_string_getter(false, action); - new_additional_tooltip = (boost::format(_utf8(L("Next Redo action: %1%"))) % action).str(); - } - - if (new_additional_tooltip != curr_additional_tooltip) { - m_undoredo_toolbar.set_additional_tooltip(id, new_additional_tooltip); - set_tooltip(""); - } - return can_redo; - }; - - if (!m_undoredo_toolbar.add_item(item)) - return false; - /* - if (!m_undoredo_toolbar.add_separator()) - return false; - */ - return true; -} - -bool GLCanvas3D::_init_view_toolbar() -{ - return wxGetApp().plater()->init_view_toolbar(); -} - -bool GLCanvas3D::_init_collapse_toolbar() -{ - return wxGetApp().plater()->init_collapse_toolbar(); -} - -bool GLCanvas3D::_set_current() -{ - return m_context != nullptr && m_canvas->SetCurrent(*m_context); -} - -void GLCanvas3D::_resize(unsigned int w, unsigned int h) -{ - if (m_canvas == nullptr && m_context == nullptr) - return; - - const std::array new_size = { w, h }; - if (m_old_size == new_size) - return; - - m_old_size = new_size; - - auto *imgui = wxGetApp().imgui(); - imgui->set_display_size(static_cast(w), static_cast(h)); - const float font_size = 1.5f * wxGetApp().em_unit(); -#if ENABLE_RETINA_GL - imgui->set_scaling(font_size, 1.0f, m_retina_helper->get_scale_factor()); -#else - imgui->set_scaling(font_size, m_canvas->GetContentScaleFactor(), 1.0f); -#endif - - this->request_extra_frame(); - - // ensures that this canvas is current - _set_current(); -} - -BoundingBoxf3 GLCanvas3D::_max_bounding_box(bool include_gizmos, bool include_bed_model) const -{ - BoundingBoxf3 bb = volumes_bounding_box(); - - // The following is a workaround for gizmos not being taken in account when calculating the tight camera frustrum - // A better solution would ask the gizmo manager for the bounding box of the current active gizmo, if any - if (include_gizmos && m_gizmos.is_running()) { - const BoundingBoxf3 sel_bb = m_selection.get_bounding_box(); - const Vec3d sel_bb_center = sel_bb.center(); - const Vec3d extend_by = sel_bb.max_size() * Vec3d::Ones(); - bb.merge(BoundingBoxf3(sel_bb_center - extend_by, sel_bb_center + extend_by)); - } - - const BoundingBoxf3 bed_bb = include_bed_model ? m_bed.extended_bounding_box() : m_bed.build_volume().bounding_volume(); - bb.merge(bed_bb); - - if (!m_main_toolbar.is_enabled()) - bb.merge(m_gcode_viewer.get_max_bounding_box()); - - // clamp max bb size with respect to bed bb size - if (!m_picking_enabled) { - static const double max_scale_factor = 2.0; - const Vec3d bb_size = bb.size(); - const Vec3d bed_bb_size = m_bed.build_volume().bounding_volume().size(); - - if ((bed_bb_size.x() > 0.0 && bb_size.x() > max_scale_factor * bed_bb_size.x()) || - (bed_bb_size.y() > 0.0 && bb_size.y() > max_scale_factor * bed_bb_size.y()) || - (bed_bb_size.z() > 0.0 && bb_size.z() > max_scale_factor * bed_bb_size.z())) { - const Vec3d bed_bb_center = bed_bb.center(); - const Vec3d extend_by = max_scale_factor * bed_bb_size; - bb = BoundingBoxf3(bed_bb_center - extend_by, bed_bb_center + extend_by); - } - } - - return bb; -} - -void GLCanvas3D::_zoom_to_box(const BoundingBoxf3& box, double margin_factor) -{ - wxGetApp().plater()->get_camera().zoom_to_box(box, margin_factor); - m_dirty = true; -} - -void GLCanvas3D::_update_camera_zoom(double zoom) -{ - wxGetApp().plater()->get_camera().update_zoom(zoom); - m_dirty = true; -} - -void GLCanvas3D::_refresh_if_shown_on_screen() -{ - if (_is_shown_on_screen()) { - const Size& cnv_size = get_canvas_size(); - _resize((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height()); - - // Because of performance problems on macOS, where PaintEvents are not delivered - // frequently enough, we call render() here directly when we can. - render(); - } -} - -#if ENABLE_RAYCAST_PICKING -void GLCanvas3D::_picking_pass() -{ - if (!m_picking_enabled || m_mouse.dragging || m_mouse.position == Vec2d(DBL_MAX, DBL_MAX) || m_gizmos.is_dragging()) { -#if ENABLE_RAYCAST_PICKING_DEBUG - ImGuiWrapper& imgui = *wxGetApp().imgui(); - imgui.begin(std::string("Hit result"), ImGuiWindowFlags_AlwaysAutoResize); - imgui.text("Picking disabled"); - imgui.end(); -#endif // ENABLE_RAYCAST_PICKING_DEBUG - return; - } - - m_hover_volume_idxs.clear(); - - const ClippingPlane clipping_plane = m_gizmos.get_clipping_plane().inverted_normal(); - const SceneRaycaster::HitResult hit = m_scene_raycaster.hit(m_mouse.position, wxGetApp().plater()->get_camera(), &clipping_plane); - if (hit.is_valid()) { - switch (hit.type) - { - case SceneRaycaster::EType::Volume: - { - if (0 <= hit.raycaster_id && hit.raycaster_id < (int)m_volumes.volumes.size()) { - const GLVolume* volume = m_volumes.volumes[hit.raycaster_id]; - if (volume->is_active && !volume->disabled && (volume->composite_id.volume_id >= 0 || m_render_sla_auxiliaries)) { - // do not add the volume id if any gizmo is active and CTRL is pressed - if (m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined || !wxGetKeyState(WXK_CONTROL)) { - m_hover_volume_idxs.emplace_back(hit.raycaster_id); - m_gizmos.set_hover_id(-1); - } - } - } - else - assert(false); - - break; - } - case SceneRaycaster::EType::Gizmo: - { - const Size& cnv_size = get_canvas_size(); - bool inside = 0 <= m_mouse.position.x() && m_mouse.position.x() < cnv_size.get_width() && - 0 <= m_mouse.position.y() && m_mouse.position.y() < cnv_size.get_height(); - m_gizmos.set_hover_id(inside ? hit.raycaster_id : -1); - break; - } - case SceneRaycaster::EType::Bed: - { - m_gizmos.set_hover_id(-1); - break; - } - default: - { - assert(false); - break; - } - } - } - else - m_gizmos.set_hover_id(-1); - - _update_volumes_hover_state(); - -#if ENABLE_RAYCAST_PICKING_DEBUG - ImGuiWrapper& imgui = *wxGetApp().imgui(); - imgui.begin(std::string("Hit result"), ImGuiWindowFlags_AlwaysAutoResize); - std::string object_type = "None"; - switch (hit.type) - { - case SceneRaycaster::EType::Bed: { object_type = "Bed"; break; } - case SceneRaycaster::EType::Gizmo: { object_type = "Gizmo element"; break; } - case SceneRaycaster::EType::Volume: - { - if (m_volumes.volumes[hit.raycaster_id]->is_wipe_tower) - object_type = "Volume (Wipe tower)"; - else if (m_volumes.volumes[hit.raycaster_id]->volume_idx() == -int(slaposPad)) - object_type = "Volume (SLA pad)"; - else if (m_volumes.volumes[hit.raycaster_id]->volume_idx() == -int(slaposSupportTree)) - object_type = "Volume (SLA supports)"; - else if (m_volumes.volumes[hit.raycaster_id]->is_modifier) - object_type = "Volume (Modifier)"; - else - object_type = "Volume (Part)"; - break; - } - default: { break; } - } - - auto add_strings_row_to_table = [&imgui](const std::string& col_1, const ImVec4& col_1_color, const std::string& col_2, const ImVec4& col_2_color) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - imgui.text_colored(col_1_color, col_1.c_str()); - ImGui::TableSetColumnIndex(1); - imgui.text_colored(col_2_color, col_2.c_str()); - }; - - char buf[1024]; - if (hit.type != SceneRaycaster::EType::None) { - if (ImGui::BeginTable("Hit", 2)) { - add_strings_row_to_table("Object ID", ImGuiWrapper::COL_ORANGE_LIGHT, std::to_string(hit.raycaster_id), ImGui::GetStyleColorVec4(ImGuiCol_Text)); - add_strings_row_to_table("Type", ImGuiWrapper::COL_ORANGE_LIGHT, object_type, ImGui::GetStyleColorVec4(ImGuiCol_Text)); - sprintf(buf, "%.3f, %.3f, %.3f", hit.position.x(), hit.position.y(), hit.position.z()); - add_strings_row_to_table("Position", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text)); - sprintf(buf, "%.3f, %.3f, %.3f", hit.normal.x(), hit.normal.y(), hit.normal.z()); - add_strings_row_to_table("Normal", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text)); - ImGui::EndTable(); - } - } - else - imgui.text("NO HIT"); - - ImGui::Separator(); - imgui.text("Registered for picking:"); - if (ImGui::BeginTable("Raycasters", 2)) { - sprintf(buf, "%d (%d)", (int)m_scene_raycaster.beds_count(), (int)m_scene_raycaster.active_beds_count()); - add_strings_row_to_table("Beds", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text)); - sprintf(buf, "%d (%d)", (int)m_scene_raycaster.volumes_count(), (int)m_scene_raycaster.active_volumes_count()); - add_strings_row_to_table("Volumes", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text)); - sprintf(buf, "%d (%d)", (int)m_scene_raycaster.gizmos_count(), (int)m_scene_raycaster.active_gizmos_count()); - add_strings_row_to_table("Gizmo elements", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text)); - ImGui::EndTable(); - } - imgui.end(); -#endif // ENABLE_RAYCAST_PICKING_DEBUG -} -#else -void GLCanvas3D::_picking_pass() -{ - if (m_picking_enabled && !m_mouse.dragging && m_mouse.position != Vec2d(DBL_MAX, DBL_MAX) && !m_gizmos.is_dragging()) { - m_hover_volume_idxs.clear(); - - // Render the object for picking. - // FIXME This cannot possibly work in a multi - sampled context as the color gets mangled by the anti - aliasing. - // Better to use software ray - casting on a bounding - box hierarchy. - - if (m_multisample_allowed) - // This flag is often ignored by NVIDIA drivers if rendering into a screen buffer. - glsafe(::glDisable(GL_MULTISAMPLE)); - - glsafe(::glDisable(GL_BLEND)); - glsafe(::glEnable(GL_DEPTH_TEST)); - - glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - m_camera_clipping_plane = m_gizmos.get_clipping_plane(); - if (m_camera_clipping_plane.is_active()) { - ::glClipPlane(GL_CLIP_PLANE0, (GLdouble*)m_camera_clipping_plane.get_data().data()); - ::glEnable(GL_CLIP_PLANE0); - } -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - _render_volumes_for_picking(); -#if !ENABLE_LEGACY_OPENGL_REMOVAL - if (m_camera_clipping_plane.is_active()) - ::glDisable(GL_CLIP_PLANE0); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Camera& camera = wxGetApp().plater()->get_camera(); - _render_bed_for_picking(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward()); -#else - _render_bed_for_picking(!wxGetApp().plater()->get_camera().is_looking_downward()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - m_gizmos.render_current_gizmo_for_picking_pass(); - - if (m_multisample_allowed) - glsafe(::glEnable(GL_MULTISAMPLE)); - - int volume_id = -1; - int gizmo_id = -1; - - std::array color = { 0, 0, 0, 0 }; - const Size& cnv_size = get_canvas_size(); - bool inside = 0 <= m_mouse.position(0) && m_mouse.position(0) < cnv_size.get_width() && 0 <= m_mouse.position(1) && m_mouse.position(1) < cnv_size.get_height(); - if (inside) { - glsafe(::glReadPixels(m_mouse.position(0), cnv_size.get_height() - m_mouse.position.y() - 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, (void*)color.data())); - if (picking_checksum_alpha_channel(color[0], color[1], color[2]) == color[3]) { - // Only non-interpolated colors are valid, those have their lowest three bits zeroed. - // we reserve color = (0,0,0) for occluders (as the printbed) - // volumes' id are shifted by 1 - // see: _render_volumes_for_picking() - unsigned int id = picking_encode(color[0], color[1], color[2]); - volume_id = id - 1; - // gizmos' id are instead properly encoded by the color - gizmo_id = id; - } - } - if (0 <= volume_id && volume_id < (int)m_volumes.volumes.size()) { - // do not add the volume id if any gizmo is active and CTRL is pressed - if (m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined || !wxGetKeyState(WXK_CONTROL)) - m_hover_volume_idxs.emplace_back(volume_id); - m_gizmos.set_hover_id(-1); - } - else - m_gizmos.set_hover_id(inside && (unsigned int)gizmo_id <= GLGizmoBase::BASE_ID ? ((int)GLGizmoBase::BASE_ID - gizmo_id) : -1); - - _update_volumes_hover_state(); - } -} -#endif // ENABLE_RAYCAST_PICKING - -void GLCanvas3D::_rectangular_selection_picking_pass() -{ - m_gizmos.set_hover_id(-1); - - std::set idxs; - - if (m_picking_enabled) { -#if ENABLE_RAYCAST_PICKING - const size_t width = std::max(m_rectangle_selection.get_width(), 1); - const size_t height = std::max(m_rectangle_selection.get_height(), 1); - - const OpenGLManager::EFramebufferType framebuffers_type = OpenGLManager::get_framebuffers_type(); - bool use_framebuffer = framebuffers_type != OpenGLManager::EFramebufferType::Unknown; - - GLuint render_fbo = 0; - GLuint render_tex = 0; - GLuint render_depth = 0; - if (use_framebuffer) { - // setup a framebuffer which covers only the selection rectangle - if (framebuffers_type == OpenGLManager::EFramebufferType::Arb) { - glsafe(::glGenFramebuffers(1, &render_fbo)); - glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, render_fbo)); - } - else { - glsafe(::glGenFramebuffersEXT(1, &render_fbo)); - glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, render_fbo)); - } - glsafe(::glGenTextures(1, &render_tex)); - glsafe(::glBindTexture(GL_TEXTURE_2D, render_tex)); - glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)); - if (framebuffers_type == OpenGLManager::EFramebufferType::Arb) { - glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, render_tex, 0)); - glsafe(::glGenRenderbuffers(1, &render_depth)); - glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, render_depth)); -#if ENABLE_OPENGL_ES - glsafe(::glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height)); -#else - glsafe(::glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height)); -#endif // ENABLE_OPENGL_ES - glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, render_depth)); - } - else { - glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, render_tex, 0)); - glsafe(::glGenRenderbuffersEXT(1, &render_depth)); - glsafe(::glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, render_depth)); - glsafe(::glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, width, height)); - glsafe(::glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, render_depth)); - } - const GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 }; - glsafe(::glDrawBuffers(1, drawBufs)); - if (framebuffers_type == OpenGLManager::EFramebufferType::Arb) { - if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) - use_framebuffer = false; - } - else { - if (::glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) != GL_FRAMEBUFFER_COMPLETE_EXT) - use_framebuffer = false; - } - } -#endif // ENABLE_RAYCAST_PICKING - - if (m_multisample_allowed) - // This flag is often ignored by NVIDIA drivers if rendering into a screen buffer. - glsafe(::glDisable(GL_MULTISAMPLE)); - - glsafe(::glDisable(GL_BLEND)); - glsafe(::glEnable(GL_DEPTH_TEST)); - - glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); - -#if ENABLE_RAYCAST_PICKING - const Camera& main_camera = wxGetApp().plater()->get_camera(); - Camera framebuffer_camera; - const Camera* camera = &main_camera; - if (use_framebuffer) { - // setup a camera which covers only the selection rectangle - const std::array& viewport = camera->get_viewport(); - const double near_left = camera->get_near_left(); - const double near_bottom = camera->get_near_bottom(); - const double near_width = camera->get_near_width(); - const double near_height = camera->get_near_height(); - - const double ratio_x = near_width / double(viewport[2]); - const double ratio_y = near_height / double(viewport[3]); - - const double rect_near_left = near_left + double(m_rectangle_selection.get_left()) * ratio_x; - const double rect_near_bottom = near_bottom + (double(viewport[3]) - double(m_rectangle_selection.get_bottom())) * ratio_y; - double rect_near_right = near_left + double(m_rectangle_selection.get_right()) * ratio_x; - double rect_near_top = near_bottom + (double(viewport[3]) - double(m_rectangle_selection.get_top())) * ratio_y; - - if (rect_near_left == rect_near_right) - rect_near_right = rect_near_left + ratio_x; - if (rect_near_bottom == rect_near_top) - rect_near_top = rect_near_bottom + ratio_y; - - framebuffer_camera.look_at(camera->get_position(), camera->get_target(), camera->get_dir_up()); - framebuffer_camera.apply_projection(rect_near_left, rect_near_right, rect_near_bottom, rect_near_top, camera->get_near_z(), camera->get_far_z()); - framebuffer_camera.set_viewport(0, 0, width, height); - framebuffer_camera.apply_viewport(); - camera = &framebuffer_camera; - } - - _render_volumes_for_picking(*camera); -#else - _render_volumes_for_picking(); -#endif // ENABLE_RAYCAST_PICKING -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_RAYCAST_PICKING - _render_bed_for_picking(camera->get_view_matrix(), camera->get_projection_matrix(), !camera->is_looking_downward()); -#else - const Camera& camera = wxGetApp().plater()->get_camera(); - _render_bed_for_picking(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward()); -#endif // ENABLE_RAYCAST_PICKING -#else - _render_bed_for_picking(!wxGetApp().plater()->get_camera().is_looking_downward()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - if (m_multisample_allowed) - glsafe(::glEnable(GL_MULTISAMPLE)); - -#if ENABLE_RAYCAST_PICKING - const size_t px_count = width * height; - - const size_t left = use_framebuffer ? 0 : (size_t)m_rectangle_selection.get_left(); - const size_t top = use_framebuffer ? 0 : (size_t)get_canvas_size().get_height() - (size_t)m_rectangle_selection.get_top(); -#else - int width = std::max((int)m_rectangle_selection.get_width(), 1); - int height = std::max((int)m_rectangle_selection.get_height(), 1); - int px_count = width * height; - - int left = (int)m_rectangle_selection.get_left(); - int top = get_canvas_size().get_height() - (int)m_rectangle_selection.get_top(); - if (left >= 0 && top >= 0) { -#endif // ENABLE_RAYCAST_PICKING -#define USE_PARALLEL 1 -#if USE_PARALLEL - struct Pixel - { - std::array data; - // Only non-interpolated colors are valid, those have their lowest three bits zeroed. - bool valid() const { return picking_checksum_alpha_channel(data[0], data[1], data[2]) == data[3]; } - // we reserve color = (0,0,0) for occluders (as the printbed) - // volumes' id are shifted by 1 - // see: _render_volumes_for_picking() - int id() const { return data[0] + (data[1] << 8) + (data[2] << 16) - 1; } - }; - - std::vector frame(px_count); - glsafe(::glReadPixels(left, top, width, height, GL_RGBA, GL_UNSIGNED_BYTE, (void*)frame.data())); - - tbb::spin_mutex mutex; - tbb::parallel_for(tbb::blocked_range(0, frame.size(), (size_t)width), - [this, &frame, &idxs, &mutex](const tbb::blocked_range& range) { - for (size_t i = range.begin(); i < range.end(); ++i) - if (frame[i].valid()) { - int volume_id = frame[i].id(); - if (0 <= volume_id && volume_id < (int)m_volumes.volumes.size()) { - mutex.lock(); - idxs.insert(volume_id); - mutex.unlock(); - } - } - }); -#else - std::vector frame(4 * px_count); - glsafe(::glReadPixels(left, top, width, height, GL_RGBA, GL_UNSIGNED_BYTE, (void*)frame.data())); - - for (int i = 0; i < px_count; ++i) - { - int px_id = 4 * i; - int volume_id = frame[px_id] + (frame[px_id + 1] << 8) + (frame[px_id + 2] << 16); - if (0 <= volume_id && volume_id < (int)m_volumes.volumes.size()) - idxs.insert(volume_id); - } -#endif // USE_PARALLEL -#if ENABLE_RAYCAST_PICKING - if (camera != &main_camera) - main_camera.apply_viewport(); - - if (framebuffers_type == OpenGLManager::EFramebufferType::Arb) { - glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, 0)); - if (render_depth != 0) - glsafe(::glDeleteRenderbuffers(1, &render_depth)); - if (render_fbo != 0) - glsafe(::glDeleteFramebuffers(1, &render_fbo)); - } - else if (framebuffers_type == OpenGLManager::EFramebufferType::Ext) { - glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0)); - if (render_depth != 0) - glsafe(::glDeleteRenderbuffersEXT(1, &render_depth)); - if (render_fbo != 0) - glsafe(::glDeleteFramebuffersEXT(1, &render_fbo)); - } - - if (render_tex != 0) - glsafe(::glDeleteTextures(1, &render_tex)); -#else - } -#endif // ENABLE_RAYCAST_PICKING - } - - m_hover_volume_idxs.assign(idxs.begin(), idxs.end()); - _update_volumes_hover_state(); -} - -void GLCanvas3D::_render_background() -{ - bool use_error_color = false; - if (wxGetApp().is_editor()) { - use_error_color = m_dynamic_background_enabled && - (current_printer_technology() != ptSLA || !m_volumes.empty()); - - if (!m_volumes.empty()) - use_error_color &= _is_any_volume_outside().first; - else - use_error_color &= m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed(); - } - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPushMatrix()); - glsafe(::glLoadIdentity()); - glsafe(::glMatrixMode(GL_PROJECTION)); - glsafe(::glPushMatrix()); - glsafe(::glLoadIdentity()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - // Draws a bottom to top gradient over the complete screen. - glsafe(::glDisable(GL_DEPTH_TEST)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const ColorRGBA bottom_color = use_error_color ? ERROR_BG_DARK_COLOR : DEFAULT_BG_DARK_COLOR; - - if (!m_background.is_initialized()) { - m_background.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P2T2 }; - init_data.reserve_vertices(4); - init_data.reserve_indices(6); - - // vertices - init_data.add_vertex(Vec2f(-1.0f, -1.0f), Vec2f(0.0f, 0.0f)); - init_data.add_vertex(Vec2f(1.0f, -1.0f), Vec2f(1.0f, 0.0f)); - init_data.add_vertex(Vec2f(1.0f, 1.0f), Vec2f(1.0f, 1.0f)); - init_data.add_vertex(Vec2f(-1.0f, 1.0f), Vec2f(0.0f, 1.0f)); - - // indices - init_data.add_triangle(0, 1, 2); - init_data.add_triangle(2, 3, 0); - - m_background.init_from(std::move(init_data)); - } - - GLShaderProgram* shader = wxGetApp().get_shader("background"); - if (shader != nullptr) { - shader->start_using(); - shader->set_uniform("top_color", use_error_color ? ERROR_BG_LIGHT_COLOR : DEFAULT_BG_LIGHT_COLOR); - shader->set_uniform("bottom_color", bottom_color); - m_background.render(); - shader->stop_using(); - } -#else - ::glBegin(GL_QUADS); - ::glColor3fv(use_error_color ? ERROR_BG_DARK_COLOR.data(): DEFAULT_BG_DARK_COLOR.data()); - ::glVertex2f(-1.0f, -1.0f); - ::glVertex2f(1.0f, -1.0f); - - ::glColor3fv(use_error_color ? ERROR_BG_LIGHT_COLOR.data() : DEFAULT_BG_LIGHT_COLOR.data()); - ::glVertex2f(1.0f, 1.0f); - ::glVertex2f(-1.0f, 1.0f); - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - glsafe(::glEnable(GL_DEPTH_TEST)); - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPopMatrix()); - glsafe(::glMatrixMode(GL_MODELVIEW)); - glsafe(::glPopMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_axes) -#else -void GLCanvas3D::_render_bed(bool bottom, bool show_axes) -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ - float scale_factor = 1.0; -#if ENABLE_RETINA_GL - scale_factor = m_retina_helper->get_scale_factor(); -#endif // ENABLE_RETINA_GL - - bool show_texture = ! bottom || - (m_gizmos.get_current_type() != GLGizmosManager::FdmSupports - && m_gizmos.get_current_type() != GLGizmosManager::SlaSupports - && m_gizmos.get_current_type() != GLGizmosManager::Hollow - && m_gizmos.get_current_type() != GLGizmosManager::Seam - && m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes, show_texture); -#else - m_bed.render(*this, bottom, scale_factor, show_axes, show_texture); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLCanvas3D::_render_bed_for_picking(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom) -#else -void GLCanvas3D::_render_bed_for_picking(bool bottom) -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ - float scale_factor = 1.0; -#if ENABLE_RETINA_GL - scale_factor = m_retina_helper->get_scale_factor(); -#endif // ENABLE_RETINA_GL - -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_bed.render_for_picking(*this, view_matrix, projection_matrix, bottom, scale_factor); -#else - m_bed.render_for_picking(*this, bottom, scale_factor); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type) -{ - if (m_volumes.empty()) - return; - - glsafe(::glEnable(GL_DEPTH_TEST)); - - m_camera_clipping_plane = m_gizmos.get_clipping_plane(); - - if (m_picking_enabled) - // Update the layer editing selection to the first object selected, update the current object maximum Z. - m_layers_editing.select_object(*m_model, this->is_layers_editing_enabled() ? m_selection.get_object_idx() : -1); - - if (const BuildVolume &build_volume = m_bed.build_volume(); build_volume.valid()) { - switch (build_volume.type()) { - case BuildVolume::Type::Rectangle: { - const BoundingBox3Base bed_bb = build_volume.bounding_volume().inflated(BuildVolume::SceneEpsilon); - m_volumes.set_print_volume({ 0, // circle - { float(bed_bb.min.x()), float(bed_bb.min.y()), float(bed_bb.max.x()), float(bed_bb.max.y()) }, - { 0.0f, float(build_volume.max_print_height()) } }); - break; - } - case BuildVolume::Type::Circle: { - m_volumes.set_print_volume({ 1, // rectangle - { unscaled(build_volume.circle().center.x()), unscaled(build_volume.circle().center.y()), unscaled(build_volume.circle().radius + BuildVolume::SceneEpsilon), 0.0f }, - { 0.0f, float(build_volume.max_print_height() + BuildVolume::SceneEpsilon) } }); - break; - } - default: - case BuildVolume::Type::Convex: - case BuildVolume::Type::Custom: { - m_volumes.set_print_volume({ static_cast(type), - { -FLT_MAX, -FLT_MAX, FLT_MAX, FLT_MAX }, - { -FLT_MAX, FLT_MAX } } - ); - } - } - if (m_requires_check_outside_state) { - m_volumes.check_outside_state(build_volume, nullptr); - m_requires_check_outside_state = false; - } - } - - if (m_use_clipping_planes) - m_volumes.set_z_range(-m_clipping_planes[0].get_data()[3], m_clipping_planes[1].get_data()[3]); - else - m_volumes.set_z_range(-FLT_MAX, FLT_MAX); - - m_volumes.set_clipping_plane(m_camera_clipping_plane.get_data()); - m_volumes.set_show_sinking_contours(! m_gizmos.is_hiding_instances()); - m_volumes.set_show_non_manifold_edges(!m_gizmos.is_hiding_instances() && m_gizmos.get_current_type() != GLGizmosManager::Simplify); - - GLShaderProgram* shader = wxGetApp().get_shader("gouraud"); - if (shader != nullptr) { - shader->start_using(); - - switch (type) - { - default: - case GLVolumeCollection::ERenderType::Opaque: - { - if (m_picking_enabled && !m_gizmos.is_dragging() && m_layers_editing.is_enabled() && (m_layers_editing.last_object_id != -1) && (m_layers_editing.object_max_z() > 0.0f)) { - int object_id = m_layers_editing.last_object_id; -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Camera& camera = wxGetApp().plater()->get_camera(); - m_volumes.render(type, false, camera.get_view_matrix(), camera.get_projection_matrix(), [object_id](const GLVolume& volume) { - // Which volume to paint without the layer height profile shader? - return volume.is_active && (volume.is_modifier || volume.composite_id.object_id != object_id); - }); -#else - m_volumes.render(type, false, wxGetApp().plater()->get_camera().get_view_matrix(), [object_id](const GLVolume& volume) { - // Which volume to paint without the layer height profile shader? - return volume.is_active && (volume.is_modifier || volume.composite_id.object_id != object_id); - }); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - // Let LayersEditing handle rendering of the active object using the layer height profile shader. - m_layers_editing.render_volumes(*this, m_volumes); - } - else { - // do not cull backfaces to show broken geometry, if any -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Camera& camera = wxGetApp().plater()->get_camera(); - m_volumes.render(type, m_picking_enabled, camera.get_view_matrix(), camera.get_projection_matrix(), [this](const GLVolume& volume) { - return (m_render_sla_auxiliaries || volume.composite_id.volume_id >= 0); - }); -#else - m_volumes.render(type, m_picking_enabled, wxGetApp().plater()->get_camera().get_view_matrix(), [this](const GLVolume& volume) { - return (m_render_sla_auxiliaries || volume.composite_id.volume_id >= 0); - }); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - - // In case a painting gizmo is open, it should render the painted triangles - // before transparent objects are rendered. Otherwise they would not be - // visible when inside modifier meshes etc. - { - GLGizmosManager& gm = get_gizmos_manager(); -// GLGizmosManager::EType type = gm.get_current_type(); - if (dynamic_cast(gm.get_current())) { - shader->stop_using(); - gm.render_painter_gizmo(); - shader->start_using(); - } - } - break; - } - case GLVolumeCollection::ERenderType::Transparent: - { -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Camera& camera = wxGetApp().plater()->get_camera(); - m_volumes.render(type, false, camera.get_view_matrix(), camera.get_projection_matrix()); -#else - m_volumes.render(type, false, wxGetApp().plater()->get_camera().get_view_matrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - break; - } - } - shader->stop_using(); - } - - m_camera_clipping_plane = ClippingPlane::ClipsNothing(); -} - -void GLCanvas3D::_render_gcode() -{ - m_gcode_viewer.render(); -} - -void GLCanvas3D::_render_gcode_cog() -{ - m_gcode_viewer.render_cog(); -} - -void GLCanvas3D::_render_selection() -{ - float scale_factor = 1.0; -#if ENABLE_RETINA_GL - scale_factor = m_retina_helper->get_scale_factor(); -#endif // ENABLE_RETINA_GL - - if (!m_gizmos.is_running()) - m_selection.render(scale_factor); -} - -void GLCanvas3D::_render_sequential_clearance() -{ - if (m_layers_editing.is_enabled() || m_gizmos.is_dragging()) - return; - - switch (m_gizmos.get_current_type()) - { - case GLGizmosManager::EType::Flatten: - case GLGizmosManager::EType::Cut: - case GLGizmosManager::EType::Hollow: - case GLGizmosManager::EType::SlaSupports: - case GLGizmosManager::EType::FdmSupports: - case GLGizmosManager::EType::Seam: { return; } - default: { break; } - } - - m_sequential_print_clearance.render(); -} - -#if ENABLE_RENDER_SELECTION_CENTER -void GLCanvas3D::_render_selection_center() -{ - m_selection.render_center(m_gizmos.is_dragging()); -} -#endif // ENABLE_RENDER_SELECTION_CENTER - -void GLCanvas3D::_check_and_update_toolbar_icon_scale() -{ - // Don't update a toolbar scale, when we are on a Preview - if (wxGetApp().plater()->is_preview_shown()) - return; - - float scale = wxGetApp().toolbar_icon_scale(); - Size cnv_size = get_canvas_size(); - - float size = GLToolbar::Default_Icons_Size * scale; - - // Set current size for all top toolbars. It will be used for next calculations - GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar(); -#if ENABLE_RETINA_GL - const float sc = m_retina_helper->get_scale_factor() * scale; - m_main_toolbar.set_scale(sc); - m_undoredo_toolbar.set_scale(sc); - collapse_toolbar.set_scale(sc); - size *= m_retina_helper->get_scale_factor(); -#else - m_main_toolbar.set_icons_size(size); - m_undoredo_toolbar.set_icons_size(size); - collapse_toolbar.set_icons_size(size); -#endif // ENABLE_RETINA_GL - - float top_tb_width = m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar.get_width(); - int items_cnt = m_main_toolbar.get_visible_items_cnt() + m_undoredo_toolbar.get_visible_items_cnt() + collapse_toolbar.get_visible_items_cnt(); - float noitems_width = top_tb_width - size * items_cnt; // width of separators and borders in top toolbars - - // calculate scale needed for items in all top toolbars - // the std::max() is there because on some Linux dialects/virtual machines this code is called when the canvas has not been properly initialized yet, - // leading to negative values for the scale. - // See: https://github.com/prusa3d/PrusaSlicer/issues/8563 - // https://github.com/supermerill/SuperSlicer/issues/854 - float new_h_scale = std::max((cnv_size.get_width() - noitems_width), 1.0f) / (items_cnt * GLToolbar::Default_Icons_Size); - - items_cnt = m_gizmos.get_selectable_icons_cnt() + 3; // +3 means a place for top and view toolbars and separators in gizmos toolbar - - // calculate scale needed for items in the gizmos toolbar - float new_v_scale = cnv_size.get_height() / (items_cnt * GLGizmosManager::Default_Icons_Size); - - // set minimum scale as a auto scale for the toolbars - float new_scale = std::min(new_h_scale, new_v_scale); -#if ENABLE_RETINA_GL - new_scale /= m_retina_helper->get_scale_factor(); -#endif - if (fabs(new_scale - scale) > 0.01) // scale is changed by 1% and more - wxGetApp().set_auto_toolbar_icon_scale(new_scale); -} - -void GLCanvas3D::_render_overlays() -{ - glsafe(::glDisable(GL_DEPTH_TEST)); -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPushMatrix()); - glsafe(::glLoadIdentity()); - // ensure that the textures are renderered inside the frustrum - const Camera& camera = wxGetApp().plater()->get_camera(); - glsafe(::glTranslated(0.0, 0.0, -(camera.get_near_z() + 0.005))); - // ensure that the overlay fits the frustrum near z plane - double gui_scale = camera.get_gui_scale(); - glsafe(::glScaled(gui_scale, gui_scale, 1.0)); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - _check_and_update_toolbar_icon_scale(); - - _render_gizmos_overlay(); - - // main toolbar and undoredo toolbar need to be both updated before rendering because both their sizes are needed - // to correctly place them -#if ENABLE_RETINA_GL - const float scale = m_retina_helper->get_scale_factor() * wxGetApp().toolbar_icon_scale(/*true*/); - m_main_toolbar.set_scale(scale); - m_undoredo_toolbar.set_scale(scale); - wxGetApp().plater()->get_collapse_toolbar().set_scale(scale); -#else - const float size = int(GLToolbar::Default_Icons_Size * wxGetApp().toolbar_icon_scale(/*true*/)); - m_main_toolbar.set_icons_size(size); - m_undoredo_toolbar.set_icons_size(size); - wxGetApp().plater()->get_collapse_toolbar().set_icons_size(size); -#endif // ENABLE_RETINA_GL - - _render_main_toolbar(); - _render_undoredo_toolbar(); - _render_collapse_toolbar(); - _render_view_toolbar(); - - if (m_layers_editing.last_object_id >= 0 && m_layers_editing.object_max_z() > 0.0f) - m_layers_editing.render_overlay(*this); - - const ConfigOptionBool* opt = dynamic_cast(m_config->option("complete_objects")); - bool sequential_print = opt != nullptr && opt->value; - std::vector sorted_instances; - if (sequential_print) { - for (ModelObject* model_object : m_model->objects) - for (ModelInstance* model_instance : model_object->instances) { - sorted_instances.emplace_back(model_instance); - } - } - m_labels.render(sorted_instances); - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPopMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_RAYCAST_PICKING -void GLCanvas3D::_render_volumes_for_picking(const Camera& camera) const -#else -void GLCanvas3D::_render_volumes_for_picking() const -#endif // ENABLE_RAYCAST_PICKING -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_shader("flat_clip"); - if (shader == nullptr) - return; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - // do not cull backfaces to show broken geometry, if any - glsafe(::glDisable(GL_CULL_FACE)); - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glEnableClientState(GL_VERTEX_ARRAY)); - glsafe(::glEnableClientState(GL_NORMAL_ARRAY)); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_RAYCAST_PICKING - const Transform3d& view_matrix = camera.get_view_matrix(); -#else - const Transform3d& view_matrix = wxGetApp().plater()->get_camera().get_view_matrix(); -#endif // ENABLE_RAYCAST_PICKING - for (size_t type = 0; type < 2; ++ type) { - GLVolumeWithIdAndZList to_render = volumes_to_render(m_volumes.volumes, (type == 0) ? GLVolumeCollection::ERenderType::Opaque : GLVolumeCollection::ERenderType::Transparent, view_matrix); - for (const GLVolumeWithIdAndZ& volume : to_render) - if (!volume.first->disabled && (volume.first->composite_id.volume_id >= 0 || m_render_sla_auxiliaries)) { - // Object picking mode. Render the object with a color encoding the object index. - // we reserve color = (0,0,0) for occluders (as the printbed) - // so we shift volumes' id by 1 to get the proper color - const unsigned int id = 1 + volume.second.first; -#if ENABLE_LEGACY_OPENGL_REMOVAL - volume.first->model.set_color(picking_decode(id)); - shader->start_using(); -#if ENABLE_RAYCAST_PICKING - shader->set_uniform("view_model_matrix", view_matrix * volume.first->world_matrix()); -#else - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("view_model_matrix", camera.get_view_matrix() * volume.first->world_matrix()); -#endif // ENABLE_RAYCAST_PICKING - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - shader->set_uniform("volume_world_matrix", volume.first->world_matrix()); - shader->set_uniform("z_range", m_volumes.get_z_range()); - shader->set_uniform("clipping_plane", m_volumes.get_clipping_plane()); -#else - glsafe(::glColor4fv(picking_decode(id).data())); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - volume.first->render(); -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - } - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glDisableClientState(GL_NORMAL_ARRAY)); - glsafe(::glDisableClientState(GL_VERTEX_ARRAY)); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - glsafe(::glEnable(GL_CULL_FACE)); -} - -void GLCanvas3D::_render_current_gizmo() const -{ - m_gizmos.render_current_gizmo(); -} - -void GLCanvas3D::_render_gizmos_overlay() -{ -#if ENABLE_RETINA_GL -// m_gizmos.set_overlay_scale(m_retina_helper->get_scale_factor()); - const float scale = m_retina_helper->get_scale_factor()*wxGetApp().toolbar_icon_scale(); - m_gizmos.set_overlay_scale(scale); //! #ys_FIXME_experiment -#else -// m_gizmos.set_overlay_scale(m_canvas->GetContentScaleFactor()); -// m_gizmos.set_overlay_scale(wxGetApp().em_unit()*0.1f); - const float size = int(GLGizmosManager::Default_Icons_Size * wxGetApp().toolbar_icon_scale()); - m_gizmos.set_overlay_icon_size(size); //! #ys_FIXME_experiment -#endif /* __WXMSW__ */ - - m_gizmos.render_overlay(); - - if (m_gizmo_highlighter.m_render_arrow) - m_gizmos.render_arrow(*this, m_gizmo_highlighter.m_gizmo_type); -} - -void GLCanvas3D::_render_main_toolbar() -{ - if (!m_main_toolbar.is_enabled()) - return; - - const Size cnv_size = get_canvas_size(); -#if ENABLE_LEGACY_OPENGL_REMOVAL - const float top = 0.5f * (float)cnv_size.get_height(); -#else - const float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom(); - const float top = 0.5f * (float)cnv_size.get_height() * inv_zoom; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar(); - const float collapse_toolbar_width = collapse_toolbar.is_enabled() ? collapse_toolbar.get_width() : 0.0f; -#if ENABLE_LEGACY_OPENGL_REMOVAL - const float left = -0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width); -#else - const float left = -0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width) * inv_zoom; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - m_main_toolbar.set_position(top, left); - m_main_toolbar.render(*this); - if (m_toolbar_highlighter.m_render_arrow) - m_main_toolbar.render_arrow(*this, m_toolbar_highlighter.m_toolbar_item); -} - -void GLCanvas3D::_render_undoredo_toolbar() -{ - if (!m_undoredo_toolbar.is_enabled()) - return; - - const Size cnv_size = get_canvas_size(); -#if ENABLE_LEGACY_OPENGL_REMOVAL - const float top = 0.5f * (float)cnv_size.get_height(); -#else - float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom(); - - const float top = 0.5f * (float)cnv_size.get_height() * inv_zoom; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar(); - const float collapse_toolbar_width = collapse_toolbar.is_enabled() ? collapse_toolbar.get_width() : 0.0f; -#if ENABLE_LEGACY_OPENGL_REMOVAL - const float left = m_main_toolbar.get_width() - 0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width); -#else - const float left = (m_main_toolbar.get_width() - 0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width)) * inv_zoom; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - m_undoredo_toolbar.set_position(top, left); - m_undoredo_toolbar.render(*this); - if (m_toolbar_highlighter.m_render_arrow) - m_undoredo_toolbar.render_arrow(*this, m_toolbar_highlighter.m_toolbar_item); -} - -void GLCanvas3D::_render_collapse_toolbar() const -{ - GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar(); - - const Size cnv_size = get_canvas_size(); - const float band = m_layers_editing.is_enabled() ? (wxGetApp().imgui()->get_style_scaling() * LayersEditing::THICKNESS_BAR_WIDTH) : 0.0; -#if ENABLE_LEGACY_OPENGL_REMOVAL - const float top = 0.5f * (float)cnv_size.get_height(); - const float left = 0.5f * (float)cnv_size.get_width() - collapse_toolbar.get_width() - band; -#else - const float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom(); - - const float top = 0.5f * (float)cnv_size.get_height() * inv_zoom; - const float left = (0.5f * (float)cnv_size.get_width() - (float)collapse_toolbar.get_width() - band) * inv_zoom; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - collapse_toolbar.set_position(top, left); - collapse_toolbar.render(*this); -} - -void GLCanvas3D::_render_view_toolbar() const -{ - GLToolbar& view_toolbar = wxGetApp().plater()->get_view_toolbar(); - -#if ENABLE_RETINA_GL - const float scale = m_retina_helper->get_scale_factor() * wxGetApp().toolbar_icon_scale(); -#if __APPLE__ - view_toolbar.set_scale(scale); -#else // if GTK3 - const float size = int(GLGizmosManager::Default_Icons_Size * scale); - view_toolbar.set_icons_size(size); -#endif // __APPLE__ -#else - const float size = int(GLGizmosManager::Default_Icons_Size * wxGetApp().toolbar_icon_scale()); - view_toolbar.set_icons_size(size); -#endif // ENABLE_RETINA_GL - - const Size cnv_size = get_canvas_size(); -#if ENABLE_LEGACY_OPENGL_REMOVAL - // places the toolbar on the bottom-left corner of the 3d scene - const float top = -0.5f * (float)cnv_size.get_height() + view_toolbar.get_height(); - const float left = -0.5f * (float)cnv_size.get_width(); -#else - float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom(); - - // places the toolbar on the bottom-left corner of the 3d scene - float top = (-0.5f * (float)cnv_size.get_height() + view_toolbar.get_height()) * inv_zoom; - float left = -0.5f * (float)cnv_size.get_width() * inv_zoom; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - view_toolbar.set_position(top, left); - view_toolbar.render(*this); -} - -#if ENABLE_SHOW_CAMERA_TARGET -void GLCanvas3D::_render_camera_target() -{ - static const float half_length = 5.0f; - - glsafe(::glDisable(GL_DEPTH_TEST)); -#if ENABLE_GL_CORE_PROFILE - if (!OpenGLManager::get_gl_info().is_core_profile()) -#endif // ENABLE_GL_CORE_PROFILE - glsafe(::glLineWidth(2.0f)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Vec3f& target = wxGetApp().plater()->get_camera().get_target().cast(); - m_camera_target.target = target.cast(); - - for (int i = 0; i < 3; ++i) { - if (!m_camera_target.axis[i].is_initialized()) { - m_camera_target.axis[i].reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.color = (i == X) ? ColorRGBA::X() : ((i == Y) ? ColorRGBA::Y() : ColorRGBA::Z()); - init_data.reserve_vertices(2); - init_data.reserve_indices(2); - - // vertices - if (i == X) { - init_data.add_vertex(Vec3f(-half_length, 0.0f, 0.0f)); - init_data.add_vertex(Vec3f(+half_length, 0.0f, 0.0f)); - } - else if (i == Y) { - init_data.add_vertex(Vec3f(0.0f, -half_length, 0.0f)); - init_data.add_vertex(Vec3f(0.0f, +half_length, 0.0f)); - } - else { - init_data.add_vertex(Vec3f(0.0f, 0.0f, -half_length)); - init_data.add_vertex(Vec3f(0.0f, 0.0f, +half_length)); - } - - // indices - init_data.add_line(0, 1); - - m_camera_target.axis[i].init_from(std::move(init_data)); - } - } - -#if ENABLE_GL_CORE_PROFILE - GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); -#else - GLShaderProgram* shader = wxGetApp().get_shader("flat"); -#endif // ENABLE_GL_CORE_PROFILE - if (shader != nullptr) { - shader->start_using(); -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("view_model_matrix", camera.get_view_matrix() * Geometry::assemble_transform(m_camera_target.target)); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#if ENABLE_GL_CORE_PROFILE - const std::array& viewport = camera.get_viewport(); - shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); - shader->set_uniform("width", 0.5f); - shader->set_uniform("gap_size", 0.0f); -#endif // ENABLE_GL_CORE_PROFILE -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - for (int i = 0; i < 3; ++i) { - m_camera_target.axis[i].render(); - } - shader->stop_using(); - } -#else - ::glBegin(GL_LINES); - const Vec3d& target = wxGetApp().plater()->get_camera().get_target(); - // draw line for x axis - ::glColor3f(1.0f, 0.0f, 0.0f); - ::glVertex3d(target.x() - half_length, target.y(), target.z()); - ::glVertex3d(target.x() + half_length, target.y(), target.z()); - // draw line for y axis - ::glColor3f(0.0f, 1.0f, 0.0f); - ::glVertex3d(target.x(), target.y() - half_length, target.z()); - ::glVertex3d(target.x(), target.y() + half_length, target.z()); - // draw line for z axis - ::glColor3f(0.0f, 0.0f, 1.0f); - ::glVertex3d(target.x(), target.y(), target.z() - half_length); - ::glVertex3d(target.x(), target.y(), target.z() + half_length); - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} -#endif // ENABLE_SHOW_CAMERA_TARGET - -void GLCanvas3D::_render_sla_slices() -{ - if (!m_use_clipping_planes || current_printer_technology() != ptSLA) - return; - - const SLAPrint* print = this->sla_print(); - const PrintObjects& print_objects = print->objects(); - if (print_objects.empty()) - // nothing to render, return - return; - - double clip_min_z = -m_clipping_planes[0].get_data()[3]; - double clip_max_z = m_clipping_planes[1].get_data()[3]; - for (unsigned int i = 0; i < (unsigned int)print_objects.size(); ++i) { - const SLAPrintObject* obj = print_objects[i]; - - if (!obj->is_step_done(slaposSliceSupports)) - continue; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - SlaCap::ObjectIdToModelsMap::iterator it_caps_bottom = m_sla_caps[0].triangles.find(i); - SlaCap::ObjectIdToModelsMap::iterator it_caps_top = m_sla_caps[1].triangles.find(i); -#else - SlaCap::ObjectIdToTrianglesMap::iterator it_caps_bottom = m_sla_caps[0].triangles.find(i); - SlaCap::ObjectIdToTrianglesMap::iterator it_caps_top = m_sla_caps[1].triangles.find(i); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - { - if (it_caps_bottom == m_sla_caps[0].triangles.end()) - it_caps_bottom = m_sla_caps[0].triangles.emplace(i, SlaCap::Triangles()).first; - if (!m_sla_caps[0].matches(clip_min_z)) { - m_sla_caps[0].z = clip_min_z; -#if ENABLE_LEGACY_OPENGL_REMOVAL - it_caps_bottom->second.object.reset(); - it_caps_bottom->second.supports.reset(); -#else - it_caps_bottom->second.object.clear(); - it_caps_bottom->second.supports.clear(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - if (it_caps_top == m_sla_caps[1].triangles.end()) - it_caps_top = m_sla_caps[1].triangles.emplace(i, SlaCap::Triangles()).first; - if (!m_sla_caps[1].matches(clip_max_z)) { - m_sla_caps[1].z = clip_max_z; -#if ENABLE_LEGACY_OPENGL_REMOVAL - it_caps_top->second.object.reset(); - it_caps_top->second.supports.reset(); -#else - it_caps_top->second.object.clear(); - it_caps_top->second.supports.clear(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - } -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLModel& bottom_obj_triangles = it_caps_bottom->second.object; - GLModel& bottom_sup_triangles = it_caps_bottom->second.supports; - GLModel& top_obj_triangles = it_caps_top->second.object; - GLModel& top_sup_triangles = it_caps_top->second.supports; -#else - Pointf3s &bottom_obj_triangles = it_caps_bottom->second.object; - Pointf3s &bottom_sup_triangles = it_caps_bottom->second.supports; - Pointf3s &top_obj_triangles = it_caps_top->second.object; - Pointf3s &top_sup_triangles = it_caps_top->second.supports; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_LEGACY_OPENGL_REMOVAL - auto init_model = [](GLModel& model, const Pointf3s& triangles, const ColorRGBA& color) { - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(triangles.size()); - init_data.reserve_indices(triangles.size() / 3); - init_data.color = color; - - unsigned int vertices_count = 0; - for (const Vec3d& v : triangles) { - init_data.add_vertex((Vec3f)v.cast()); - ++vertices_count; - if (vertices_count % 3 == 0) - init_data.add_triangle(vertices_count - 3, vertices_count - 2, vertices_count - 1); - } - - if (!init_data.is_empty()) - model.init_from(std::move(init_data)); - }; - - if ((!bottom_obj_triangles.is_initialized() || !bottom_sup_triangles.is_initialized() || - !top_obj_triangles.is_initialized() || !top_sup_triangles.is_initialized()) && !obj->get_slice_index().empty()) { -#else - if ((bottom_obj_triangles.empty() || bottom_sup_triangles.empty() || top_obj_triangles.empty() || top_sup_triangles.empty()) && - !obj->get_slice_index().empty()) { -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - double layer_height = print->default_object_config().layer_height.value; - double initial_layer_height = print->material_config().initial_layer_height.value; - bool left_handed = obj->is_left_handed(); - - coord_t key_zero = obj->get_slice_index().front().print_level(); - // Slice at the center of the slab starting at clip_min_z will be rendered for the lower plane. - coord_t key_low = coord_t((clip_min_z - initial_layer_height + layer_height) / SCALING_FACTOR) + key_zero; - // Slice at the center of the slab ending at clip_max_z will be rendered for the upper plane. - coord_t key_high = coord_t((clip_max_z - initial_layer_height) / SCALING_FACTOR) + key_zero; - - const SliceRecord& slice_low = obj->closest_slice_to_print_level(key_low, coord_t(SCALED_EPSILON)); - const SliceRecord& slice_high = obj->closest_slice_to_print_level(key_high, coord_t(SCALED_EPSILON)); - - // Offset to avoid OpenGL Z fighting between the object's horizontal surfaces and the triangluated surfaces of the cuts. - const double plane_shift_z = 0.002; - - if (slice_low.is_valid()) { - const ExPolygons& obj_bottom = slice_low.get_slice(soModel); - const ExPolygons& sup_bottom = slice_low.get_slice(soSupport); -#if ENABLE_LEGACY_OPENGL_REMOVAL - // calculate model bottom cap - if (!bottom_obj_triangles.is_initialized() && !obj_bottom.empty()) - init_model(bottom_obj_triangles, triangulate_expolygons_3d(obj_bottom, clip_min_z - plane_shift_z, !left_handed), { 1.0f, 0.37f, 0.0f, 1.0f }); - // calculate support bottom cap - if (!bottom_sup_triangles.is_initialized() && !sup_bottom.empty()) - init_model(bottom_sup_triangles, triangulate_expolygons_3d(sup_bottom, clip_min_z - plane_shift_z, !left_handed), { 1.0f, 0.0f, 0.37f, 1.0f }); -#else - // calculate model bottom cap - if (bottom_obj_triangles.empty() && !obj_bottom.empty()) - bottom_obj_triangles = triangulate_expolygons_3d(obj_bottom, clip_min_z - plane_shift_z, ! left_handed); - // calculate support bottom cap - if (bottom_sup_triangles.empty() && !sup_bottom.empty()) - bottom_sup_triangles = triangulate_expolygons_3d(sup_bottom, clip_min_z - plane_shift_z, !left_handed); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - - if (slice_high.is_valid()) { - const ExPolygons& obj_top = slice_high.get_slice(soModel); - const ExPolygons& sup_top = slice_high.get_slice(soSupport); -#if ENABLE_LEGACY_OPENGL_REMOVAL - // calculate model top cap - if (!top_obj_triangles.is_initialized() && !obj_top.empty()) - init_model(top_obj_triangles, triangulate_expolygons_3d(obj_top, clip_max_z + plane_shift_z, left_handed), { 1.0f, 0.37f, 0.0f, 1.0f }); - // calculate support top cap - if (!top_sup_triangles.is_initialized() && !sup_top.empty()) - init_model(top_sup_triangles, triangulate_expolygons_3d(sup_top, clip_max_z + plane_shift_z, left_handed), { 1.0f, 0.0f, 0.37f, 1.0f }); -#else - // calculate model top cap - if (top_obj_triangles.empty() && !obj_top.empty()) - top_obj_triangles = triangulate_expolygons_3d(obj_top, clip_max_z + plane_shift_z, left_handed); - // calculate support top cap - if (top_sup_triangles.empty() && !sup_top.empty()) - top_sup_triangles = triangulate_expolygons_3d(sup_top, clip_max_z + plane_shift_z, left_handed); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader != nullptr) { - shader->start_using(); - - for (const SLAPrintObject::Instance& inst : obj->instances()) { - const Camera& camera = wxGetApp().plater()->get_camera(); - const Transform3d view_model_matrix = camera.get_view_matrix() * - Geometry::assemble_transform(Vec3d(unscale(inst.shift.x()), unscale(inst.shift.y()), 0.0), - inst.rotation * Vec3d::UnitZ(), Vec3d::Ones(), - obj->is_left_handed() ? /* The polygons are mirrored by X */ Vec3d(-1.0f, 1.0f, 1.0f) : Vec3d::Ones()); - - shader->set_uniform("view_model_matrix", view_model_matrix); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - - bottom_obj_triangles.render(); - top_obj_triangles.render(); - bottom_sup_triangles.render(); - top_sup_triangles.render(); - } - - shader->stop_using(); - } -#else - if (!bottom_obj_triangles.empty() || !top_obj_triangles.empty() || !bottom_sup_triangles.empty() || !top_sup_triangles.empty()) { - for (const SLAPrintObject::Instance& inst : obj->instances()) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(unscale(inst.shift.x()), unscale(inst.shift.y()), 0.0)); - glsafe(::glRotatef(Geometry::rad2deg(inst.rotation), 0.0f, 0.0f, 1.0f)); - if (obj->is_left_handed()) - // The polygons are mirrored by X. - glsafe(::glScalef(-1.0f, 1.0f, 1.0f)); - glsafe(::glEnableClientState(GL_VERTEX_ARRAY)); - glsafe(::glColor3f(1.0f, 0.37f, 0.0f)); - if (!bottom_obj_triangles.empty()) { - glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)bottom_obj_triangles.front().data())); - glsafe(::glDrawArrays(GL_TRIANGLES, 0, bottom_obj_triangles.size())); - } - if (! top_obj_triangles.empty()) { - glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)top_obj_triangles.front().data())); - glsafe(::glDrawArrays(GL_TRIANGLES, 0, top_obj_triangles.size())); - } - glsafe(::glColor3f(1.0f, 0.0f, 0.37f)); - if (! bottom_sup_triangles.empty()) { - glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)bottom_sup_triangles.front().data())); - glsafe(::glDrawArrays(GL_TRIANGLES, 0, bottom_sup_triangles.size())); - } - if (! top_sup_triangles.empty()) { - glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)top_sup_triangles.front().data())); - glsafe(::glDrawArrays(GL_TRIANGLES, 0, top_sup_triangles.size())); - } - glsafe(::glDisableClientState(GL_VERTEX_ARRAY)); - glsafe(::glPopMatrix()); - } - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } -} - -void GLCanvas3D::_render_selection_sidebar_hints() -{ - m_selection.render_sidebar_hints(m_sidebar_field); -} - -void GLCanvas3D::_update_volumes_hover_state() -{ - for (GLVolume* v : m_volumes.volumes) { - v->hover = GLVolume::HS_None; - } - - if (m_hover_volume_idxs.empty()) - return; - - bool ctrl_pressed = wxGetKeyState(WXK_CONTROL); // additive select/deselect - bool shift_pressed = wxGetKeyState(WXK_SHIFT); // select by rectangle - bool alt_pressed = wxGetKeyState(WXK_ALT); // deselect by rectangle - - if (alt_pressed && (shift_pressed || ctrl_pressed)) { - // illegal combinations of keys - m_hover_volume_idxs.clear(); - return; - } - - bool hover_modifiers_only = true; - for (int i : m_hover_volume_idxs) { - if (!m_volumes.volumes[i]->is_modifier) { - hover_modifiers_only = false; - break; - } - } - - std::set> hover_instances; - for (int i : m_hover_volume_idxs) { - const GLVolume& v = *m_volumes.volumes[i]; - hover_instances.insert(std::make_pair(v.object_idx(), v.instance_idx())); - } - - bool hover_from_single_instance = hover_instances.size() == 1; - - if (hover_modifiers_only && !hover_from_single_instance) { - // do not allow to select volumes from different instances - m_hover_volume_idxs.clear(); - return; - } - - for (int i : m_hover_volume_idxs) { - GLVolume& volume = *m_volumes.volumes[i]; - if (volume.hover != GLVolume::HS_None) - continue; - - bool deselect = volume.selected && ((shift_pressed && m_rectangle_selection.is_empty()) || (alt_pressed && !m_rectangle_selection.is_empty())); - bool select = !volume.selected && (m_rectangle_selection.is_empty() || (shift_pressed && !m_rectangle_selection.is_empty())); - - if (select || deselect) { - bool as_volume = - volume.is_modifier && hover_from_single_instance && !ctrl_pressed && - ( - (!deselect) || - (deselect && !m_selection.is_single_full_instance() && (volume.object_idx() == m_selection.get_object_idx()) && (volume.instance_idx() == m_selection.get_instance_idx())) - ); - - if (as_volume) - volume.hover = deselect ? GLVolume::HS_Deselect : GLVolume::HS_Select; - else { - int object_idx = volume.object_idx(); - int instance_idx = volume.instance_idx(); - - for (GLVolume* v : m_volumes.volumes) { - if (v->object_idx() == object_idx && v->instance_idx() == instance_idx) - v->hover = deselect ? GLVolume::HS_Deselect : GLVolume::HS_Select; - } - } - } - else if (volume.selected) - volume.hover = GLVolume::HS_Hover; - } -} - -void GLCanvas3D::_perform_layer_editing_action(wxMouseEvent* evt) -{ - int object_idx_selected = m_layers_editing.last_object_id; - if (object_idx_selected == -1) - return; - - // A volume is selected. Test, whether hovering over a layer thickness bar. - if (evt != nullptr) { - const Rect& rect = LayersEditing::get_bar_rect_screen(*this); - float b = rect.get_bottom(); - m_layers_editing.last_z = m_layers_editing.object_max_z() * (b - evt->GetY() - 1.0f) / (b - rect.get_top()); - m_layers_editing.last_action = - evt->ShiftDown() ? (evt->RightIsDown() ? LAYER_HEIGHT_EDIT_ACTION_SMOOTH : LAYER_HEIGHT_EDIT_ACTION_REDUCE) : - (evt->RightIsDown() ? LAYER_HEIGHT_EDIT_ACTION_INCREASE : LAYER_HEIGHT_EDIT_ACTION_DECREASE); - } - - m_layers_editing.adjust_layer_height_profile(); - _refresh_if_shown_on_screen(); - - // Automatic action on mouse down with the same coordinate. - _start_timer(); -} - -Vec3d GLCanvas3D::_mouse_to_3d(const Point& mouse_pos, float* z) -{ - if (m_canvas == nullptr) - return Vec3d(DBL_MAX, DBL_MAX, DBL_MAX); - -#if ENABLE_RAYCAST_PICKING - if (z == nullptr) { - const SceneRaycaster::HitResult hit = m_scene_raycaster.hit(mouse_pos.cast(), wxGetApp().plater()->get_camera(), nullptr); - return hit.is_valid() ? hit.position.cast() : _mouse_to_bed_3d(mouse_pos); - } - else { - const Camera& camera = wxGetApp().plater()->get_camera(); - const Vec4i viewport(camera.get_viewport().data()); - Vec3d out; - igl::unproject(Vec3d(mouse_pos.x(), viewport[3] - mouse_pos.y(), *z), camera.get_view_matrix().matrix(), camera.get_projection_matrix().matrix(), viewport, out); - return out; - } -#else - const Camera& camera = wxGetApp().plater()->get_camera(); - const Matrix4d modelview = camera.get_view_matrix().matrix(); - const Matrix4d projection = camera.get_projection_matrix().matrix(); - const Vec4i viewport(camera.get_viewport().data()); - - const int y = viewport[3] - mouse_pos.y(); - float mouse_z; - if (z == nullptr) - glsafe(::glReadPixels(mouse_pos.x(), y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, (void*)&mouse_z)); - else - mouse_z = *z; - - Vec3d out; - igl::unproject(Vec3d(mouse_pos.x(), y, mouse_z), modelview, projection, viewport, out); - return out; -#endif // ENABLE_RAYCAST_PICKING -} - -Vec3d GLCanvas3D::_mouse_to_bed_3d(const Point& mouse_pos) -{ - return mouse_ray(mouse_pos).intersect_plane(0.0); -} - -void GLCanvas3D::_start_timer() -{ - m_timer.Start(100, wxTIMER_CONTINUOUS); -} - -void GLCanvas3D::_stop_timer() -{ - m_timer.Stop(); -} - -void GLCanvas3D::_load_print_toolpaths(const BuildVolume &build_volume) -{ - const Print *print = this->fff_print(); - if (print == nullptr) - return; - - if (! print->is_step_done(psSkirtBrim)) - return; - - if (!print->has_skirt() && !print->has_brim()) - return; - - const ColorRGBA color = ColorRGBA::GREENISH(); - - // number of skirt layers - size_t total_layer_count = 0; - for (const PrintObject* print_object : print->objects()) { - total_layer_count = std::max(total_layer_count, print_object->total_layer_count()); - } - size_t skirt_height = print->has_infinite_skirt() ? total_layer_count : std::min(print->config().skirt_height.value, total_layer_count); - if (skirt_height == 0 && print->has_brim()) - skirt_height = 1; - - // Get first skirt_height layers. - //FIXME This code is fishy. It may not work for multiple objects with different layering due to variable layer height feature. - // This is not critical as this is just an initial preview. - const PrintObject* highest_object = *std::max_element(print->objects().begin(), print->objects().end(), [](auto l, auto r){ return l->layers().size() < r->layers().size(); }); - std::vector print_zs; - print_zs.reserve(skirt_height * 2); - for (size_t i = 0; i < std::min(skirt_height, highest_object->layers().size()); ++ i) - print_zs.emplace_back(float(highest_object->layers()[i]->print_z)); - // Only add skirt for the raft layers. - for (size_t i = 0; i < std::min(skirt_height, std::min(highest_object->slicing_parameters().raft_layers(), highest_object->support_layers().size())); ++ i) - print_zs.emplace_back(float(highest_object->support_layers()[i]->print_z)); - sort_remove_duplicates(print_zs); - skirt_height = std::min(skirt_height, print_zs.size()); - print_zs.erase(print_zs.begin() + skirt_height, print_zs.end()); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLVolume* volume = m_volumes.new_toolpath_volume(color); - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; -#else - GLVolume *volume = m_volumes.new_toolpath_volume(color, VERTEX_BUFFER_RESERVE_SIZE); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - for (size_t i = 0; i < skirt_height; ++ i) { - volume->print_zs.emplace_back(print_zs[i]); -#if ENABLE_LEGACY_OPENGL_REMOVAL - volume->offsets.emplace_back(init_data.indices_count()); - if (i == 0) - _3DScene::extrusionentity_to_verts(print->brim(), print_zs[i], Point(0, 0), init_data); - _3DScene::extrusionentity_to_verts(print->skirt(), print_zs[i], Point(0, 0), init_data); -#else - volume->offsets.emplace_back(volume->indexed_vertex_array.quad_indices.size()); - volume->offsets.emplace_back(volume->indexed_vertex_array.triangle_indices.size()); - if (i == 0) - _3DScene::extrusionentity_to_verts(print->brim(), print_zs[i], Point(0, 0), *volume); - _3DScene::extrusionentity_to_verts(print->skirt(), print_zs[i], Point(0, 0), *volume); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - // Ensure that no volume grows over the limits. If the volume is too large, allocate a new one. -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (init_data.vertices_size_bytes() > MAX_VERTEX_BUFFER_SIZE) { - volume->model.init_from(std::move(init_data)); -#else - if (volume->indexed_vertex_array.vertices_and_normals_interleaved.size() > MAX_VERTEX_BUFFER_SIZE) { -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - GLVolume &vol = *volume; - volume = m_volumes.new_toolpath_volume(vol.color); -#if !ENABLE_LEGACY_OPENGL_REMOVAL - reserve_new_volume_finalize_old_volume(*volume, vol, m_initialized); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - } - } -#if ENABLE_LEGACY_OPENGL_REMOVAL - volume->model.init_from(std::move(init_data)); - volume->is_outside = !contains(build_volume, volume->model); -#else - volume->is_outside = ! build_volume.all_paths_inside_vertices_and_normals_interleaved(volume->indexed_vertex_array.vertices_and_normals_interleaved, volume->indexed_vertex_array.bounding_box()); - volume->indexed_vertex_array.finalize_geometry(m_initialized); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, const BuildVolume& build_volume, const std::vector& str_tool_colors, const std::vector& color_print_values) -{ - std::vector tool_colors; - decode_colors(str_tool_colors, tool_colors); - - struct Ctxt - { - const PrintInstances *shifted_copies; - std::vector layers; - bool has_perimeters; - bool has_infill; - bool has_support; - const std::vector* tool_colors; - bool is_single_material_print; - int extruders_cnt; - const std::vector* color_print_values; - - static ColorRGBA color_perimeters() { return ColorRGBA::YELLOW(); } - static ColorRGBA color_infill() { return ColorRGBA::REDISH(); } - static ColorRGBA color_support() { return ColorRGBA::GREENISH(); } - static ColorRGBA color_pause_or_custom_code() { return ColorRGBA::GRAY(); } - - // For cloring by a tool, return a parsed color. - bool color_by_tool() const { return tool_colors != nullptr; } - size_t number_tools() const { return color_by_tool() ? tool_colors->size() : 0; } - const ColorRGBA& color_tool(size_t tool) const { return (*tool_colors)[tool]; } - - // For coloring by a color_print(M600), return a parsed color. - bool color_by_color_print() const { return color_print_values!=nullptr; } - const size_t color_print_color_idx_by_layer_idx(const size_t layer_idx) const { - const CustomGCode::Item value{layers[layer_idx]->print_z + EPSILON, CustomGCode::Custom, 0, ""}; - auto it = std::lower_bound(color_print_values->begin(), color_print_values->end(), value); - return (it - color_print_values->begin()) % number_tools(); - } - - const size_t color_print_color_idx_by_layer_idx_and_extruder(const size_t layer_idx, const int extruder) const - { - const coordf_t print_z = layers[layer_idx]->print_z; - - auto it = std::find_if(color_print_values->begin(), color_print_values->end(), - [print_z](const CustomGCode::Item& code) - { return fabs(code.print_z - print_z) < EPSILON; }); - if (it != color_print_values->end()) { - CustomGCode::Type type = it->type; - // pause print or custom Gcode - if (type == CustomGCode::PausePrint || - (type != CustomGCode::ColorChange && type != CustomGCode::ToolChange)) - return number_tools()-1; // last color item is a gray color for pause print or custom G-code - - // change tool (extruder) - if (type == CustomGCode::ToolChange) - return get_color_idx_for_tool_change(it, extruder); - // change color for current extruder - if (type == CustomGCode::ColorChange) { - int color_idx = get_color_idx_for_color_change(it, extruder); - if (color_idx >= 0) - return color_idx; - } - } - - const CustomGCode::Item value{print_z + EPSILON, CustomGCode::Custom, 0, ""}; - it = std::lower_bound(color_print_values->begin(), color_print_values->end(), value); - while (it != color_print_values->begin()) { - --it; - // change color for current extruder - if (it->type == CustomGCode::ColorChange) { - int color_idx = get_color_idx_for_color_change(it, extruder); - if (color_idx >= 0) - return color_idx; - } - // change tool (extruder) - if (it->type == CustomGCode::ToolChange) - return get_color_idx_for_tool_change(it, extruder); - } - - return std::min(extruders_cnt - 1, std::max(extruder - 1, 0));; - } - - private: - int get_m600_color_idx(std::vector::const_iterator it) const - { - int shift = 0; - while (it != color_print_values->begin()) { - --it; - if (it->type == CustomGCode::ColorChange) - shift++; - } - return extruders_cnt + shift; - } - - int get_color_idx_for_tool_change(std::vector::const_iterator it, const int extruder) const - { - const int current_extruder = it->extruder == 0 ? extruder : it->extruder; - if (number_tools() == size_t(extruders_cnt + 1)) // there is no one "M600" - return std::min(extruders_cnt - 1, std::max(current_extruder - 1, 0)); - - auto it_n = it; - while (it_n != color_print_values->begin()) { - --it_n; - if (it_n->type == CustomGCode::ColorChange && it_n->extruder == current_extruder) - return get_m600_color_idx(it_n); - } - - return std::min(extruders_cnt - 1, std::max(current_extruder - 1, 0)); - } - - int get_color_idx_for_color_change(std::vector::const_iterator it, const int extruder) const - { - if (extruders_cnt == 1) - return get_m600_color_idx(it); - - auto it_n = it; - bool is_tool_change = false; - while (it_n != color_print_values->begin()) { - --it_n; - if (it_n->type == CustomGCode::ToolChange) { - is_tool_change = true; - if (it_n->extruder == it->extruder || (it_n->extruder == 0 && it->extruder == extruder)) - return get_m600_color_idx(it); - break; - } - } - if (!is_tool_change && it->extruder == extruder) - return get_m600_color_idx(it); - - return -1; - } - - } ctxt; - - ctxt.has_perimeters = print_object.is_step_done(posPerimeters); - ctxt.has_infill = print_object.is_step_done(posInfill); - ctxt.has_support = print_object.is_step_done(posSupportMaterial); - ctxt.tool_colors = tool_colors.empty() ? nullptr : &tool_colors; - ctxt.color_print_values = color_print_values.empty() ? nullptr : &color_print_values; - ctxt.is_single_material_print = this->fff_print()->extruders().size()==1; - ctxt.extruders_cnt = wxGetApp().extruders_edited_cnt(); - - ctxt.shifted_copies = &print_object.instances(); - - // order layers by print_z - { - size_t nlayers = 0; - if (ctxt.has_perimeters || ctxt.has_infill) - nlayers = print_object.layers().size(); - if (ctxt.has_support) - nlayers += print_object.support_layers().size(); - ctxt.layers.reserve(nlayers); - } - if (ctxt.has_perimeters || ctxt.has_infill) - for (const Layer *layer : print_object.layers()) - ctxt.layers.emplace_back(layer); - if (ctxt.has_support) - for (const Layer *layer : print_object.support_layers()) - ctxt.layers.emplace_back(layer); - std::sort(ctxt.layers.begin(), ctxt.layers.end(), [](const Layer *l1, const Layer *l2) { return l1->print_z < l2->print_z; }); - - // Maximum size of an allocation block: 32MB / sizeof(float) - BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - start" << m_volumes.log_memory_info() << log_memory_info(); - - const bool is_selected_separate_extruder = m_selected_extruder > 0 && ctxt.color_by_color_print(); - - //FIXME Improve the heuristics for a grain size. - size_t grain_size = std::max(ctxt.layers.size() / 16, size_t(1)); - tbb::spin_mutex new_volume_mutex; - auto new_volume = [this, &new_volume_mutex](const ColorRGBA& color) { - // Allocate the volume before locking. - GLVolume *volume = new GLVolume(color); - volume->is_extrusion_path = true; -#if ENABLE_LEGACY_OPENGL_REMOVAL - // to prevent sending data to gpu (in the main thread) while - // editing the model geometry - volume->model.disable_render(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - tbb::spin_mutex::scoped_lock lock; - // Lock by ROII, so if the emplace_back() fails, the lock will be released. - lock.acquire(new_volume_mutex); - m_volumes.volumes.emplace_back(volume); - lock.release(); - return volume; - }; - const size_t volumes_cnt_initial = m_volumes.volumes.size(); - // Limit the number of threads as the code below does not scale well due to memory pressure. - // (most of the time is spent in malloc / free / memmove) - // Not using all the threads leaves some of the threads to G-code generator. - tbb::task_arena limited_arena(std::min(tbb::this_task_arena::max_concurrency(), 4)); - limited_arena.execute([&ctxt, grain_size, &new_volume, is_selected_separate_extruder, this]{ - tbb::parallel_for( - tbb::blocked_range(0, ctxt.layers.size(), grain_size), - [&ctxt, &new_volume, is_selected_separate_extruder, this](const tbb::blocked_range& range) { - GLVolumePtrs vols; -#if ENABLE_LEGACY_OPENGL_REMOVAL - std::vector geometries; - auto select_geometry = [&ctxt, &geometries](size_t layer_idx, int extruder, int feature) -> GLModel::Geometry& { - return geometries[ctxt.color_by_color_print() ? - ctxt.color_print_color_idx_by_layer_idx_and_extruder(layer_idx, extruder) : - ctxt.color_by_tool() ? - std::min(ctxt.number_tools() - 1, std::max(extruder - 1, 0)) : - feature - ]; - }; -#else - auto volume = [&ctxt, &vols](size_t layer_idx, int extruder, int feature) -> GLVolume& { - return *vols[ctxt.color_by_color_print() ? - ctxt.color_print_color_idx_by_layer_idx_and_extruder(layer_idx, extruder) : - ctxt.color_by_tool() ? - std::min(ctxt.number_tools() - 1, std::max(extruder - 1, 0)) : - feature - ]; - }; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - if (ctxt.color_by_color_print() || ctxt.color_by_tool()) { - for (size_t i = 0; i < ctxt.number_tools(); ++i) { - vols.emplace_back(new_volume(ctxt.color_tool(i))); -#if ENABLE_LEGACY_OPENGL_REMOVAL - geometries.emplace_back(GLModel::Geometry()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - } - else { - vols = { new_volume(ctxt.color_perimeters()), new_volume(ctxt.color_infill()), new_volume(ctxt.color_support()) }; -#if ENABLE_LEGACY_OPENGL_REMOVAL - geometries = { GLModel::Geometry(), GLModel::Geometry(), GLModel::Geometry() }; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - assert(vols.size() == geometries.size()); - for (GLModel::Geometry& g : geometries) { - g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; - } -#else - for (GLVolume *vol : vols) - // Reserving number of vertices (3x position + 3x color) - vol->indexed_vertex_array.reserve(VERTEX_BUFFER_RESERVE_SIZE / 6); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) { - const Layer *layer = ctxt.layers[idx_layer]; - - if (is_selected_separate_extruder) { - bool at_least_one_has_correct_extruder = false; - for (const LayerRegion* layerm : layer->regions()) { - if (layerm->slices.surfaces.empty()) - continue; - const PrintRegionConfig& cfg = layerm->region().config(); - if (cfg.perimeter_extruder.value == m_selected_extruder || - cfg.infill_extruder.value == m_selected_extruder || - cfg.solid_infill_extruder.value == m_selected_extruder ) { - at_least_one_has_correct_extruder = true; - break; - } - } - if (!at_least_one_has_correct_extruder) - continue; - } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - for (size_t i = 0; i < vols.size(); ++i) { - GLVolume* vol = vols[i]; - if (vol->print_zs.empty() || vol->print_zs.back() != layer->print_z) { - vol->print_zs.emplace_back(layer->print_z); - vol->offsets.emplace_back(geometries[i].indices_count()); - } - } -#else - for (GLVolume* vol : vols) - if (vol->print_zs.empty() || vol->print_zs.back() != layer->print_z) { - vol->print_zs.emplace_back(layer->print_z); - vol->offsets.emplace_back(vol->indexed_vertex_array.quad_indices.size()); - vol->offsets.emplace_back(vol->indexed_vertex_array.triangle_indices.size()); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - for (const PrintInstance &instance : *ctxt.shifted_copies) { - const Point © = instance.shift; - for (const LayerRegion *layerm : layer->regions()) { - if (is_selected_separate_extruder) { - const PrintRegionConfig& cfg = layerm->region().config(); - if (cfg.perimeter_extruder.value != m_selected_extruder || - cfg.infill_extruder.value != m_selected_extruder || - cfg.solid_infill_extruder.value != m_selected_extruder) - continue; - } - if (ctxt.has_perimeters) -#if ENABLE_LEGACY_OPENGL_REMOVAL - _3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy, - select_geometry(idx_layer, layerm->region().config().perimeter_extruder.value, 0)); -#else - _3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy, - volume(idx_layer, layerm->region().config().perimeter_extruder.value, 0)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - if (ctxt.has_infill) { - for (const ExtrusionEntity *ee : layerm->fills.entities) { - // fill represents infill extrusions of a single island. - const auto *fill = dynamic_cast(ee); - if (! fill->entities.empty()) -#if ENABLE_LEGACY_OPENGL_REMOVAL - _3DScene::extrusionentity_to_verts(*fill, float(layer->print_z), copy, - select_geometry(idx_layer, is_solid_infill(fill->entities.front()->role()) ? - layerm->region().config().solid_infill_extruder : - layerm->region().config().infill_extruder, 1)); -#else - _3DScene::extrusionentity_to_verts(*fill, float(layer->print_z), copy, - volume(idx_layer, - is_solid_infill(fill->entities.front()->role()) ? - layerm->region().config().solid_infill_extruder : - layerm->region().config().infill_extruder, - 1)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - } - } - if (ctxt.has_support) { - const SupportLayer *support_layer = dynamic_cast(layer); - if (support_layer) { - for (const ExtrusionEntity *extrusion_entity : support_layer->support_fills.entities) -#if ENABLE_LEGACY_OPENGL_REMOVAL - _3DScene::extrusionentity_to_verts(extrusion_entity, float(layer->print_z), copy, - select_geometry(idx_layer, (extrusion_entity->role() == erSupportMaterial) ? - support_layer->object()->config().support_material_extruder : - support_layer->object()->config().support_material_interface_extruder, 2)); -#else - _3DScene::extrusionentity_to_verts(extrusion_entity, float(layer->print_z), copy, - volume(idx_layer, - (extrusion_entity->role() == erSupportMaterial) ? - support_layer->object()->config().support_material_extruder : - support_layer->object()->config().support_material_interface_extruder, - 2)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - } - } - // Ensure that no volume grows over the limits. If the volume is too large, allocate a new one. - for (size_t i = 0; i < vols.size(); ++i) { - GLVolume &vol = *vols[i]; -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (geometries[i].vertices_size_bytes() > MAX_VERTEX_BUFFER_SIZE) { - vol.model.init_from(std::move(geometries[i])); -#else - if (vol.indexed_vertex_array.vertices_and_normals_interleaved.size() > MAX_VERTEX_BUFFER_SIZE) { -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - vols[i] = new_volume(vol.color); -#if !ENABLE_LEGACY_OPENGL_REMOVAL - reserve_new_volume_finalize_old_volume(*vols[i], vol, false); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - } - } - } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - for (size_t i = 0; i < vols.size(); ++i) { - if (!geometries[i].is_empty()) - vols[i]->model.init_from(std::move(geometries[i])); - } -#else - for (GLVolume *vol : vols) - // Ideally one would call vol->indexed_vertex_array.finalize() here to move the buffers to the OpenGL driver, - // but this code runs in parallel and the OpenGL driver is not thread safe. - vol->indexed_vertex_array.shrink_to_fit(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - }); - }); // task arena - - BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - finalizing results" << m_volumes.log_memory_info() << log_memory_info(); - // Remove empty volumes from the newly added volumes. - { - for (auto ptr_it = m_volumes.volumes.begin() + volumes_cnt_initial; ptr_it != m_volumes.volumes.end(); ++ptr_it) - if ((*ptr_it)->empty()) { - delete *ptr_it; - *ptr_it = nullptr; - } - m_volumes.volumes.erase(std::remove(m_volumes.volumes.begin() + volumes_cnt_initial, m_volumes.volumes.end(), nullptr), m_volumes.volumes.end()); - } - for (size_t i = volumes_cnt_initial; i < m_volumes.volumes.size(); ++i) { - GLVolume* v = m_volumes.volumes[i]; -#if ENABLE_LEGACY_OPENGL_REMOVAL - v->is_outside = !contains(build_volume, v->model); - // We are done editinig the model, now it can be sent to gpu - v->model.enable_render(); -#else - v->is_outside = ! build_volume.all_paths_inside_vertices_and_normals_interleaved(v->indexed_vertex_array.vertices_and_normals_interleaved, v->indexed_vertex_array.bounding_box()); - v->indexed_vertex_array.finalize_geometry(m_initialized); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - - BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - end" << m_volumes.log_memory_info() << log_memory_info(); -} - -void GLCanvas3D::_load_wipe_tower_toolpaths(const BuildVolume& build_volume, const std::vector& str_tool_colors) -{ - const Print *print = this->fff_print(); - if (print == nullptr || print->wipe_tower_data().tool_changes.empty()) - return; - - if (!print->is_step_done(psWipeTower)) - return; - - std::vector tool_colors; - decode_colors(str_tool_colors, tool_colors); - - struct Ctxt - { - const Print *print; - const std::vector *tool_colors; - Vec2f wipe_tower_pos; - float wipe_tower_angle; - - static ColorRGBA color_support() { return ColorRGBA::GREENISH(); } - - // For cloring by a tool, return a parsed color. - bool color_by_tool() const { return tool_colors != nullptr; } - size_t number_tools() const { return this->color_by_tool() ? tool_colors->size() : 0; } - const ColorRGBA& color_tool(size_t tool) const { return (*tool_colors)[tool]; } - int volume_idx(int tool, int feature) const { - return this->color_by_tool() ? std::min(this->number_tools() - 1, std::max(tool, 0)) : feature; - } - - const std::vector& tool_change(size_t idx) { - const auto &tool_changes = print->wipe_tower_data().tool_changes; - return priming.empty() ? - ((idx == tool_changes.size()) ? final : tool_changes[idx]) : - ((idx == 0) ? priming : (idx == tool_changes.size() + 1) ? final : tool_changes[idx - 1]); - } - std::vector priming; - std::vector final; - } ctxt; - - ctxt.print = print; - ctxt.tool_colors = tool_colors.empty() ? nullptr : &tool_colors; - if (print->wipe_tower_data().priming && print->config().single_extruder_multi_material_priming) - for (int i=0; i<(int)print->wipe_tower_data().priming.get()->size(); ++i) - ctxt.priming.emplace_back(print->wipe_tower_data().priming.get()->at(i)); - if (print->wipe_tower_data().final_purge) - ctxt.final.emplace_back(*print->wipe_tower_data().final_purge.get()); - - ctxt.wipe_tower_angle = ctxt.print->config().wipe_tower_rotation_angle.value/180.f * PI; - ctxt.wipe_tower_pos = Vec2f(ctxt.print->config().wipe_tower_x.value, ctxt.print->config().wipe_tower_y.value); - - BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - start" << m_volumes.log_memory_info() << log_memory_info(); - - //FIXME Improve the heuristics for a grain size. - size_t n_items = print->wipe_tower_data().tool_changes.size() + (ctxt.priming.empty() ? 0 : 1); - size_t grain_size = std::max(n_items / 128, size_t(1)); - tbb::spin_mutex new_volume_mutex; - auto new_volume = [this, &new_volume_mutex](const ColorRGBA& color) { - auto *volume = new GLVolume(color); - volume->is_extrusion_path = true; -#if ENABLE_LEGACY_OPENGL_REMOVAL - // to prevent sending data to gpu (in the main thread) while - // editing the model geometry - volume->model.disable_render(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - tbb::spin_mutex::scoped_lock lock; - lock.acquire(new_volume_mutex); - m_volumes.volumes.emplace_back(volume); - lock.release(); - return volume; - }; - const size_t volumes_cnt_initial = m_volumes.volumes.size(); - std::vector volumes_per_thread(n_items); - tbb::parallel_for( - tbb::blocked_range(0, n_items, grain_size), - [&ctxt, &new_volume](const tbb::blocked_range& range) { - // Bounding box of this slab of a wipe tower. - GLVolumePtrs vols; -#if ENABLE_LEGACY_OPENGL_REMOVAL - std::vector geometries; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - if (ctxt.color_by_tool()) { - for (size_t i = 0; i < ctxt.number_tools(); ++i) { - vols.emplace_back(new_volume(ctxt.color_tool(i))); -#if ENABLE_LEGACY_OPENGL_REMOVAL - geometries.emplace_back(GLModel::Geometry()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - } - else { - vols = { new_volume(ctxt.color_support()) }; -#if ENABLE_LEGACY_OPENGL_REMOVAL - geometries = { GLModel::Geometry() }; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - assert(vols.size() == geometries.size()); - for (GLModel::Geometry& g : geometries) { - g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; - } -#else - for (GLVolume *volume : vols) - // Reserving number of vertices (3x position + 3x color) - volume->indexed_vertex_array.reserve(VERTEX_BUFFER_RESERVE_SIZE / 6); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) { - const std::vector &layer = ctxt.tool_change(idx_layer); - for (size_t i = 0; i < vols.size(); ++i) { - GLVolume &vol = *vols[i]; - if (vol.print_zs.empty() || vol.print_zs.back() != layer.front().print_z) { - vol.print_zs.emplace_back(layer.front().print_z); -#if ENABLE_LEGACY_OPENGL_REMOVAL - vol.offsets.emplace_back(geometries[i].indices_count()); -#else - vol.offsets.emplace_back(vol.indexed_vertex_array.quad_indices.size()); - vol.offsets.emplace_back(vol.indexed_vertex_array.triangle_indices.size()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - } - for (const WipeTower::ToolChangeResult &extrusions : layer) { - for (size_t i = 1; i < extrusions.extrusions.size();) { - const WipeTower::Extrusion &e = extrusions.extrusions[i]; - if (e.width == 0.) { - ++i; - continue; - } - size_t j = i + 1; - if (ctxt.color_by_tool()) - for (; j < extrusions.extrusions.size() && extrusions.extrusions[j].tool == e.tool && extrusions.extrusions[j].width > 0.f; ++j); - else - for (; j < extrusions.extrusions.size() && extrusions.extrusions[j].width > 0.f; ++j); - size_t n_lines = j - i; - Lines lines; - std::vector widths; - std::vector heights; - lines.reserve(n_lines); - widths.reserve(n_lines); - heights.assign(n_lines, extrusions.layer_height); - WipeTower::Extrusion e_prev = extrusions.extrusions[i-1]; - - if (!extrusions.priming) { // wipe tower extrusions describe the wipe tower at the origin with no rotation - e_prev.pos = Eigen::Rotation2Df(ctxt.wipe_tower_angle) * e_prev.pos; - e_prev.pos += ctxt.wipe_tower_pos; - } - - for (; i < j; ++i) { - WipeTower::Extrusion e = extrusions.extrusions[i]; - assert(e.width > 0.f); - if (!extrusions.priming) { - e.pos = Eigen::Rotation2Df(ctxt.wipe_tower_angle) * e.pos; - e.pos += ctxt.wipe_tower_pos; - } - - lines.emplace_back(Point::new_scale(e_prev.pos.x(), e_prev.pos.y()), Point::new_scale(e.pos.x(), e.pos.y())); - widths.emplace_back(e.width); - - e_prev = e; - } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - _3DScene::thick_lines_to_verts(lines, widths, heights, lines.front().a == lines.back().b, extrusions.print_z, - geometries[ctxt.volume_idx(e.tool, 0)]); -#else - _3DScene::thick_lines_to_verts(lines, widths, heights, lines.front().a == lines.back().b, extrusions.print_z, - *vols[ctxt.volume_idx(e.tool, 0)]); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - } - } - for (size_t i = 0; i < vols.size(); ++i) { - GLVolume &vol = *vols[i]; -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (geometries[i].vertices_size_bytes() > MAX_VERTEX_BUFFER_SIZE) { - vol.model.init_from(std::move(geometries[i])); -#else - if (vol.indexed_vertex_array.vertices_and_normals_interleaved.size() > MAX_VERTEX_BUFFER_SIZE) { -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - vols[i] = new_volume(vol.color); -#if !ENABLE_LEGACY_OPENGL_REMOVAL - reserve_new_volume_finalize_old_volume(*vols[i], vol, false); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - } - } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - for (size_t i = 0; i < vols.size(); ++i) { - if (!geometries[i].is_empty()) - vols[i]->model.init_from(std::move(geometries[i])); - } -#else - for (GLVolume *vol : vols) - vol->indexed_vertex_array.shrink_to_fit(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - }); - - BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - finalizing results" << m_volumes.log_memory_info() << log_memory_info(); - // Remove empty volumes from the newly added volumes. - { - for (auto ptr_it = m_volumes.volumes.begin() + volumes_cnt_initial; ptr_it != m_volumes.volumes.end(); ++ptr_it) - if ((*ptr_it)->empty()) { - delete *ptr_it; - *ptr_it = nullptr; - } - m_volumes.volumes.erase(std::remove(m_volumes.volumes.begin() + volumes_cnt_initial, m_volumes.volumes.end(), nullptr), m_volumes.volumes.end()); - } - for (size_t i = volumes_cnt_initial; i < m_volumes.volumes.size(); ++i) { - GLVolume* v = m_volumes.volumes[i]; -#if ENABLE_LEGACY_OPENGL_REMOVAL - v->is_outside = !contains(build_volume, v->model); - // We are done editinig the model, now it can be sent to gpu - v->model.enable_render(); -#else - v->is_outside = ! build_volume.all_paths_inside_vertices_and_normals_interleaved(v->indexed_vertex_array.vertices_and_normals_interleaved, v->indexed_vertex_array.bounding_box()); - v->indexed_vertex_array.finalize_geometry(m_initialized); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - - BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - end" << m_volumes.log_memory_info() << log_memory_info(); -} - -// While it looks like we can call -// this->reload_scene(true, true) -// the two functions are quite different: -// 1) This function only loads objects, for which the step slaposSliceSupports already finished. Therefore objects outside of the print bed never load. -// 2) This function loads object mesh with the relative scaling correction (the "relative_correction" parameter) was applied, -// therefore the mesh may be slightly larger or smaller than the mesh shown in the 3D scene. -void GLCanvas3D::_load_sla_shells() -{ - const SLAPrint* print = this->sla_print(); - if (print->objects().empty()) - // nothing to render, return - return; - - auto add_volume = [this](const SLAPrintObject &object, int volume_id, const SLAPrintObject::Instance& instance, - const TriangleMesh& mesh, const ColorRGBA& color, bool outside_printer_detection_enabled) { - m_volumes.volumes.emplace_back(new GLVolume(color)); - GLVolume& v = *m_volumes.volumes.back(); -#if ENABLE_SMOOTH_NORMALS -#if ENABLE_LEGACY_OPENGL_REMOVAL - v.model.init_from(mesh, true); -#else - v.indexed_vertex_array.load_mesh(mesh, true); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#else -#if ENABLE_LEGACY_OPENGL_REMOVAL - v.model.init_from(mesh); -#else - v.indexed_vertex_array.load_mesh(mesh); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#endif // ENABLE_SMOOTH_NORMALS -#if !ENABLE_LEGACY_OPENGL_REMOVAL - v.indexed_vertex_array.finalize_geometry(m_initialized); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - v.shader_outside_printer_detection_enabled = outside_printer_detection_enabled; - v.composite_id.volume_id = volume_id; - v.set_instance_offset(unscale(instance.shift.x(), instance.shift.y(), 0.0)); - v.set_instance_rotation({ 0.0, 0.0, (double)instance.rotation }); - v.set_instance_mirror(X, object.is_left_handed() ? -1. : 1.); - v.set_convex_hull(mesh.convex_hull_3d()); - }; - - // adds objects' volumes - for (const SLAPrintObject* obj : print->objects()) - if (obj->is_step_done(slaposSliceSupports)) { - unsigned int initial_volumes_count = (unsigned int)m_volumes.volumes.size(); - for (const SLAPrintObject::Instance& instance : obj->instances()) { - add_volume(*obj, 0, instance, obj->get_mesh_to_print(), GLVolume::MODEL_COLOR[0], true); - // Set the extruder_id and volume_id to achieve the same color as in the 3D scene when - // through the update_volumes_colors_by_extruder() call. - m_volumes.volumes.back()->extruder_id = obj->model_object()->volumes.front()->extruder_id(); - if (obj->is_step_done(slaposSupportTree) && obj->has_mesh(slaposSupportTree)) - add_volume(*obj, -int(slaposSupportTree), instance, obj->support_mesh(), GLVolume::SLA_SUPPORT_COLOR, true); - if (obj->is_step_done(slaposPad) && obj->has_mesh(slaposPad)) - add_volume(*obj, -int(slaposPad), instance, obj->pad_mesh(), GLVolume::SLA_PAD_COLOR, false); - } - double shift_z = obj->get_current_elevation(); - for (unsigned int i = initial_volumes_count; i < m_volumes.volumes.size(); ++ i) { - // apply shift z - m_volumes.volumes[i]->set_sla_shift_z(shift_z); - } - } - - update_volumes_colors_by_extruder(); -} - -void GLCanvas3D::_update_sla_shells_outside_state() -{ - check_volumes_outside_state(); -} - -void GLCanvas3D::_set_warning_notification_if_needed(EWarning warning) -{ - _set_current(); - bool show = false; - if (!m_volumes.empty()) { - if (current_printer_technology() == ptSLA) { - const auto [res, volume] = _is_any_volume_outside(); - if (res) { - if (warning == EWarning::ObjectClashed) - show = !volume->is_sla_support(); - else if (warning == EWarning::SlaSupportsOutside) - show = volume->is_sla_support(); - } - } - else - show = _is_any_volume_outside().first; - } - else { - if (wxGetApp().is_editor()) { - if (current_printer_technology() != ptSLA) - show = m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed(); - } - } - - _set_warning_notification(warning, show); -} - -void GLCanvas3D::_set_warning_notification(EWarning warning, bool state) -{ - enum ErrorType{ - PLATER_WARNING, - PLATER_ERROR, - SLICING_ERROR - }; - std::string text; - ErrorType error = ErrorType::PLATER_WARNING; - switch (warning) { - case EWarning::ObjectOutside: text = _u8L("An object outside the print area was detected."); break; - case EWarning::ToolpathOutside: text = _u8L("A toolpath outside the print area was detected."); error = ErrorType::SLICING_ERROR; break; - case EWarning::SlaSupportsOutside: text = _u8L("SLA supports outside the print area were detected."); error = ErrorType::PLATER_ERROR; break; - case EWarning::SomethingNotShown: text = _u8L("Some objects are not visible during editing."); break; - case EWarning::ObjectClashed: - text = _u8L("An object outside the print area was detected.\n" - "Resolve the current problem to continue slicing."); - error = ErrorType::PLATER_ERROR; - break; - } - auto& notification_manager = *wxGetApp().plater()->get_notification_manager(); - switch (error) - { - case PLATER_WARNING: - if (state) - notification_manager.push_plater_warning_notification(text); - else - notification_manager.close_plater_warning_notification(text); - break; - case PLATER_ERROR: - if (state) - notification_manager.push_plater_error_notification(text); - else - notification_manager.close_plater_error_notification(text); - break; - case SLICING_ERROR: - if (state) - notification_manager.push_slicing_error_notification(text); - else - notification_manager.close_slicing_error_notification(text); - break; - default: - break; - } -} - -std::pair GLCanvas3D::_is_any_volume_outside() const -{ - for (const GLVolume* volume : m_volumes.volumes) { - if (volume != nullptr && volume->is_outside) - return std::make_pair(true, volume); - } - - return std::make_pair(false, nullptr); -} - -void GLCanvas3D::_update_selection_from_hover() -{ - bool ctrl_pressed = wxGetKeyState(WXK_CONTROL); - bool selection_changed = false; - - if (m_hover_volume_idxs.empty()) { - if (!ctrl_pressed && m_rectangle_selection.get_state() == GLSelectionRectangle::EState::Select) { - selection_changed = ! m_selection.is_empty(); - m_selection.remove_all(); - } - } - - GLSelectionRectangle::EState state = m_rectangle_selection.get_state(); - - bool hover_modifiers_only = true; - for (int i : m_hover_volume_idxs) { - if (!m_volumes.volumes[i]->is_modifier) { - hover_modifiers_only = false; - break; - } - } - - if (!m_rectangle_selection.is_empty()) { - if (state == GLSelectionRectangle::EState::Select) { - bool contains_all = true; - for (int i : m_hover_volume_idxs) { - if (!m_selection.contains_volume((unsigned int)i)) { - contains_all = false; - break; - } - } - - // the selection is going to be modified (Add) - if (!contains_all) { - wxGetApp().plater()->take_snapshot(_L("Selection-Add from rectangle"), UndoRedo::SnapshotType::Selection); - selection_changed = true; - } - } - else { - bool contains_any = false; - for (int i : m_hover_volume_idxs) { - if (m_selection.contains_volume((unsigned int)i)) { - contains_any = true; - break; - } - } - - // the selection is going to be modified (Remove) - if (contains_any) { - wxGetApp().plater()->take_snapshot(_L("Selection-Remove from rectangle"), UndoRedo::SnapshotType::Selection); - selection_changed = true; - } - } - } - - if (!selection_changed) - return; - - Plater::SuppressSnapshots suppress(wxGetApp().plater()); - - if (state == GLSelectionRectangle::EState::Select && !ctrl_pressed) - m_selection.clear(); - - for (int i : m_hover_volume_idxs) { - if (state == GLSelectionRectangle::EState::Select) { - if (hover_modifiers_only) { - const GLVolume& v = *m_volumes.volumes[i]; - m_selection.add_volume(v.object_idx(), v.volume_idx(), v.instance_idx(), false); - } - else - m_selection.add(i, false); - } - else - m_selection.remove(i); - } - - if (m_selection.is_empty()) - m_gizmos.reset_all_states(); - else - m_gizmos.refresh_on_off_state(); - - m_gizmos.update_data(); - post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); - m_dirty = true; -} - -bool GLCanvas3D::_deactivate_undo_redo_toolbar_items() -{ - if (m_undoredo_toolbar.is_item_pressed("undo")) { - m_undoredo_toolbar.force_right_action(m_undoredo_toolbar.get_item_id("undo"), *this); - return true; - } - else if (m_undoredo_toolbar.is_item_pressed("redo")) { - m_undoredo_toolbar.force_right_action(m_undoredo_toolbar.get_item_id("redo"), *this); - return true; - } - - return false; -} - -bool GLCanvas3D::is_search_pressed() const -{ - return m_main_toolbar.is_item_pressed("search"); -} - -bool GLCanvas3D::_deactivate_arrange_menu() -{ - if (m_main_toolbar.is_item_pressed("arrange")) { - m_main_toolbar.force_right_action(m_main_toolbar.get_item_id("arrange"), *this); - return true; - } - - return false; -} - -bool GLCanvas3D::_deactivate_search_toolbar_item() -{ - if (is_search_pressed()) { - m_main_toolbar.force_left_action(m_main_toolbar.get_item_id("search"), *this); - return true; - } - - return false; -} - -bool GLCanvas3D::_activate_search_toolbar_item() -{ - if (!m_main_toolbar.is_item_pressed("search")) { - m_main_toolbar.force_left_action(m_main_toolbar.get_item_id("search"), *this); - return true; - } - - return false; -} - -bool GLCanvas3D::_deactivate_collapse_toolbar_items() -{ - GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar(); - if (collapse_toolbar.is_item_pressed("print")) { - collapse_toolbar.force_left_action(collapse_toolbar.get_item_id("print"), *this); - return true; - } - - return false; -} - -void GLCanvas3D::highlight_toolbar_item(const std::string& item_name) -{ - GLToolbarItem* item = m_main_toolbar.get_item(item_name); - if (!item) - item = m_undoredo_toolbar.get_item(item_name); - if (!item || !item->is_visible()) - return; - m_toolbar_highlighter.init(item, this); -} - -void GLCanvas3D::highlight_gizmo(const std::string& gizmo_name) -{ - GLGizmosManager::EType gizmo = m_gizmos.get_gizmo_from_name(gizmo_name); - if(gizmo == GLGizmosManager::EType::Undefined) - return; - m_gizmo_highlighter.init(&m_gizmos, gizmo, this); -} - -const Print* GLCanvas3D::fff_print() const -{ - return (m_process == nullptr) ? nullptr : m_process->fff_print(); -} - -const SLAPrint* GLCanvas3D::sla_print() const -{ - return (m_process == nullptr) ? nullptr : m_process->sla_print(); -} - -void GLCanvas3D::WipeTowerInfo::apply_wipe_tower() const -{ - DynamicPrintConfig cfg; - cfg.opt("wipe_tower_x", true)->value = m_pos(X); - cfg.opt("wipe_tower_y", true)->value = m_pos(Y); - cfg.opt("wipe_tower_rotation_angle", true)->value = (180./M_PI) * m_rotation; - wxGetApp().get_tab(Preset::TYPE_PRINT)->load_config(cfg); -} - -void GLCanvas3D::RenderTimer::Notify() -{ - wxPostEvent((wxEvtHandler*)GetOwner(), RenderTimerEvent( EVT_GLCANVAS_RENDER_TIMER, *this)); -} - -void GLCanvas3D::ToolbarHighlighterTimer::Notify() -{ - wxPostEvent((wxEvtHandler*)GetOwner(), ToolbarHighlighterTimerEvent(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, *this)); -} - -void GLCanvas3D::GizmoHighlighterTimer::Notify() -{ - wxPostEvent((wxEvtHandler*)GetOwner(), GizmoHighlighterTimerEvent(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, *this)); -} - -void GLCanvas3D::ToolbarHighlighter::set_timer_owner(wxEvtHandler* owner, int timerid/* = wxID_ANY*/) -{ - m_timer.SetOwner(owner, timerid); -} - -void GLCanvas3D::ToolbarHighlighter::init(GLToolbarItem* toolbar_item, GLCanvas3D* canvas) -{ - if (m_timer.IsRunning()) - invalidate(); - if (!toolbar_item || !canvas) - return; - - m_timer.Start(300, false); - - m_toolbar_item = toolbar_item; - m_canvas = canvas; -} - -void GLCanvas3D::ToolbarHighlighter::invalidate() -{ - m_timer.Stop(); - - if (m_toolbar_item) { - m_toolbar_item->set_highlight(GLToolbarItem::EHighlightState::NotHighlighted); - } - m_toolbar_item = nullptr; - m_blink_counter = 0; - m_render_arrow = false; -} - -void GLCanvas3D::ToolbarHighlighter::blink() -{ - if (m_toolbar_item) { - char state = m_toolbar_item->get_highlight(); - if (state != (char)GLToolbarItem::EHighlightState::HighlightedShown) - m_toolbar_item->set_highlight(GLToolbarItem::EHighlightState::HighlightedShown); - else - m_toolbar_item->set_highlight(GLToolbarItem::EHighlightState::HighlightedHidden); - - m_render_arrow = !m_render_arrow; - m_canvas->set_as_dirty(); - } - else - invalidate(); - - if ((++m_blink_counter) >= 11) - invalidate(); -} - -void GLCanvas3D::GizmoHighlighter::set_timer_owner(wxEvtHandler* owner, int timerid/* = wxID_ANY*/) -{ - m_timer.SetOwner(owner, timerid); -} - -void GLCanvas3D::GizmoHighlighter::init(GLGizmosManager* manager, GLGizmosManager::EType gizmo, GLCanvas3D* canvas) -{ - if (m_timer.IsRunning()) - invalidate(); - if (!gizmo || !canvas) - return; - - m_timer.Start(300, false); - - m_gizmo_manager = manager; - m_gizmo_type = gizmo; - m_canvas = canvas; -} - -void GLCanvas3D::GizmoHighlighter::invalidate() -{ - m_timer.Stop(); - - if (m_gizmo_manager) { - m_gizmo_manager->set_highlight(GLGizmosManager::EType::Undefined, false); - } - m_gizmo_manager = nullptr; - m_gizmo_type = GLGizmosManager::EType::Undefined; - m_blink_counter = 0; - m_render_arrow = false; -} - -void GLCanvas3D::GizmoHighlighter::blink() -{ - if (m_gizmo_manager) { - if (m_blink_counter % 2 == 0) - m_gizmo_manager->set_highlight(m_gizmo_type, true); - else - m_gizmo_manager->set_highlight(m_gizmo_type, false); - - m_render_arrow = !m_render_arrow; - m_canvas->set_as_dirty(); - } - else - invalidate(); - - if ((++m_blink_counter) >= 11) - invalidate(); -} - -} // namespace GUI -} // namespace Slic3r +#include "libslic3r/libslic3r.h" +#include "GLCanvas3D.hpp" + +#include + +#include "libslic3r/BuildVolume.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/GCode/ThumbnailData.hpp" +#include "libslic3r/Geometry/ConvexHull.hpp" +#include "libslic3r/ExtrusionEntity.hpp" +#include "libslic3r/Layer.hpp" +#include "libslic3r/Utils.hpp" +#include "libslic3r/LocalesUtils.hpp" +#include "libslic3r/Technologies.hpp" +#include "libslic3r/Tesselate.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "3DBed.hpp" +#include "3DScene.hpp" +#include "BackgroundSlicingProcess.hpp" +#include "GLShader.hpp" +#include "GUI.hpp" +#include "Tab.hpp" +#include "GUI_Preview.hpp" +#include "OpenGLManager.hpp" +#include "Plater.hpp" +#include "MainFrame.hpp" +#include "GUI_App.hpp" +#include "GUI_ObjectList.hpp" +#include "GUI_ObjectManipulation.hpp" +#include "Mouse3DController.hpp" +#include "I18N.hpp" +#include "NotificationManager.hpp" +#include "format.hpp" + +#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp" +#include "slic3r/Utils/UndoRedo.hpp" + +#if ENABLE_RETINA_GL +#include "slic3r/Utils/RetinaHelper.hpp" +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// Print now includes tbb, and tbb includes Windows. This breaks compilation of wxWidgets if included before wx. +#include "libslic3r/Print.hpp" +#include "libslic3r/SLAPrint.hpp" + +#include "wxExtensions.hpp" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include "DoubleSlider.hpp" + +#include + +static constexpr const float TRACKBALLSIZE = 0.8f; + +#if ENABLE_LEGACY_OPENGL_REMOVAL +static const Slic3r::ColorRGBA DEFAULT_BG_DARK_COLOR = { 0.478f, 0.478f, 0.478f, 1.0f }; +static const Slic3r::ColorRGBA DEFAULT_BG_LIGHT_COLOR = { 0.753f, 0.753f, 0.753f, 1.0f }; +static const Slic3r::ColorRGBA ERROR_BG_DARK_COLOR = { 0.478f, 0.192f, 0.039f, 1.0f }; +static const Slic3r::ColorRGBA ERROR_BG_LIGHT_COLOR = { 0.753f, 0.192f, 0.039f, 1.0f }; +#else +static const Slic3r::ColorRGB DEFAULT_BG_DARK_COLOR = { 0.478f, 0.478f, 0.478f }; +static const Slic3r::ColorRGB DEFAULT_BG_LIGHT_COLOR = { 0.753f, 0.753f, 0.753f }; +static const Slic3r::ColorRGB ERROR_BG_DARK_COLOR = { 0.478f, 0.192f, 0.039f }; +static const Slic3r::ColorRGB ERROR_BG_LIGHT_COLOR = { 0.753f, 0.192f, 0.039f }; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +// Number of floats +static constexpr const size_t MAX_VERTEX_BUFFER_SIZE = 131072 * 6; // 3.15MB +// Reserve size in number of floats. +#if !ENABLE_LEGACY_OPENGL_REMOVAL +static constexpr const size_t VERTEX_BUFFER_RESERVE_SIZE = 131072 * 2; // 1.05MB +// Reserve size in number of floats, maximum sum of all preallocated buffers. +//static constexpr const size_t VERTEX_BUFFER_RESERVE_SIZE_SUM_MAX = 1024 * 1024 * 128 / 4; // 128MB +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + +namespace Slic3r { +namespace GUI { + +#ifdef __WXGTK3__ +// wxGTK3 seems to simulate OSX behavior in regard to HiDPI scaling support. +RetinaHelper::RetinaHelper(wxWindow* window) : m_window(window), m_self(nullptr) {} +RetinaHelper::~RetinaHelper() {} +float RetinaHelper::get_scale_factor() { return float(m_window->GetContentScaleFactor()); } +#endif // __WXGTK3__ + +// Fixed the collision between BuildVolume::Type::Convex and macro Convex defined inside /usr/include/X11/X.h that is included by WxWidgets 3.0. +#if defined(__linux__) && defined(Convex) +#undef Convex +#endif + +GLCanvas3D::LayersEditing::~LayersEditing() +{ + if (m_z_texture_id != 0) { + glsafe(::glDeleteTextures(1, &m_z_texture_id)); + m_z_texture_id = 0; + } + delete m_slicing_parameters; +} + +const float GLCanvas3D::LayersEditing::THICKNESS_BAR_WIDTH = 70.0f; + +void GLCanvas3D::LayersEditing::init() +{ + glsafe(::glGenTextures(1, (GLuint*)&m_z_texture_id)); + glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id)); + if (!OpenGLManager::get_gl_info().is_core_profile() || !OpenGLManager::get_gl_info().is_mesa()) { + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); + } + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1)); + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); +} + +void GLCanvas3D::LayersEditing::set_config(const DynamicPrintConfig* config) +{ + m_config = config; + delete m_slicing_parameters; + m_slicing_parameters = nullptr; + m_layers_texture.valid = false; +} + +void GLCanvas3D::LayersEditing::select_object(const Model &model, int object_id) +{ + const ModelObject *model_object_new = (object_id >= 0) ? model.objects[object_id] : nullptr; + // Maximum height of an object changes when the object gets rotated or scaled. + // Changing maximum height of an object will invalidate the layer heigth editing profile. + // m_model_object->bounding_box() is cached, therefore it is cheap even if this method is called frequently. + const float new_max_z = (model_object_new == nullptr) ? 0.0f : static_cast(model_object_new->bounding_box().max.z()); + if (m_model_object != model_object_new || this->last_object_id != object_id || m_object_max_z != new_max_z || + (model_object_new != nullptr && m_model_object->id() != model_object_new->id())) { + m_layer_height_profile.clear(); + m_layer_height_profile_modified = false; + delete m_slicing_parameters; + m_slicing_parameters = nullptr; + m_layers_texture.valid = false; + this->last_object_id = object_id; + m_model_object = model_object_new; + m_object_max_z = new_max_z; + } +} + +bool GLCanvas3D::LayersEditing::is_allowed() const +{ + return wxGetApp().get_shader("variable_layer_height") != nullptr && m_z_texture_id > 0; +} + +bool GLCanvas3D::LayersEditing::is_enabled() const +{ + return m_enabled; +} + +void GLCanvas3D::LayersEditing::set_enabled(bool enabled) +{ + m_enabled = is_allowed() && enabled; +} + +float GLCanvas3D::LayersEditing::s_overlay_window_width; + +void GLCanvas3D::LayersEditing::render_overlay(const GLCanvas3D& canvas) +{ + if (!m_enabled) + return; + + const Size& cnv_size = canvas.get_canvas_size(); + + ImGuiWrapper& imgui = *wxGetApp().imgui(); + imgui.set_next_window_pos(static_cast(cnv_size.get_width()) - imgui.get_style_scaling() * THICKNESS_BAR_WIDTH, + static_cast(cnv_size.get_height()), ImGuiCond_Always, 1.0f, 1.0f); + + imgui.begin(_L("Variable layer height"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse); + + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Left mouse button:")); + ImGui::SameLine(); + imgui.text(_L("Add detail")); + + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Right mouse button:")); + ImGui::SameLine(); + imgui.text(_L("Remove detail")); + + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Shift + Left mouse button:")); + ImGui::SameLine(); + imgui.text(_L("Reset to base")); + + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Shift + Right mouse button:")); + ImGui::SameLine(); + imgui.text(_L("Smoothing")); + + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Mouse wheel:")); + ImGui::SameLine(); + imgui.text(_L("Increase/decrease edit area")); + + ImGui::Separator(); + if (imgui.button(_L("Adaptive"))) + wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), Event(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, m_adaptive_quality)); + + ImGui::SameLine(); + float text_align = ImGui::GetCursorPosX(); + ImGui::AlignTextToFramePadding(); + imgui.text(_L("Quality / Speed")); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(_L("Higher print quality versus higher print speed.").ToUTF8()); + ImGui::EndTooltip(); + } + + ImGui::SameLine(); + float widget_align = ImGui::GetCursorPosX(); + ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f); + m_adaptive_quality = std::clamp(m_adaptive_quality, 0.0f, 1.f); + imgui.slider_float("", &m_adaptive_quality, 0.0f, 1.f, "%.2f"); + + ImGui::Separator(); + if (imgui.button(_L("Smooth"))) + wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), HeightProfileSmoothEvent(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, m_smooth_params)); + + ImGui::SameLine(); + ImGui::SetCursorPosX(text_align); + ImGui::AlignTextToFramePadding(); + imgui.text(_L("Radius")); + ImGui::SameLine(); + ImGui::SetCursorPosX(widget_align); + ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f); + int radius = (int)m_smooth_params.radius; + if (ImGui::SliderInt("##1", &radius, 1, 10)) { + radius = std::clamp(radius, 1, 10); + m_smooth_params.radius = (unsigned int)radius; + } + + ImGui::SetCursorPosX(text_align); + ImGui::AlignTextToFramePadding(); + imgui.text(_L("Keep min")); + ImGui::SameLine(); + if (ImGui::GetCursorPosX() < widget_align) // because of line lenght after localization + ImGui::SetCursorPosX(widget_align); + + ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f); + imgui.checkbox("##2", m_smooth_params.keep_min); + + ImGui::Separator(); + if (imgui.button(_L("Reset"))) + wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), SimpleEvent(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE)); + + GLCanvas3D::LayersEditing::s_overlay_window_width = ImGui::GetWindowSize().x /*+ (float)m_layers_texture.width/4*/; + imgui.end(); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + render_active_object_annotations(canvas); + render_profile(canvas); +#else + const Rect& bar_rect = get_bar_rect_viewport(canvas); + render_active_object_annotations(canvas, bar_rect); + render_profile(bar_rect); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +float GLCanvas3D::LayersEditing::get_cursor_z_relative(const GLCanvas3D& canvas) +{ + const Vec2d mouse_pos = canvas.get_local_mouse_position(); + const Rect& rect = get_bar_rect_screen(canvas); + float x = (float)mouse_pos.x(); + float y = (float)mouse_pos.y(); + float t = rect.get_top(); + float b = rect.get_bottom(); + + return (rect.get_left() <= x && x <= rect.get_right() && t <= y && y <= b) ? + // Inside the bar. + (b - y - 1.0f) / (b - t - 1.0f) : + // Outside the bar. + -1000.0f; +} + +bool GLCanvas3D::LayersEditing::bar_rect_contains(const GLCanvas3D& canvas, float x, float y) +{ + const Rect& rect = get_bar_rect_screen(canvas); + return rect.get_left() <= x && x <= rect.get_right() && rect.get_top() <= y && y <= rect.get_bottom(); +} + +Rect GLCanvas3D::LayersEditing::get_bar_rect_screen(const GLCanvas3D& canvas) +{ + const Size& cnv_size = canvas.get_canvas_size(); + float w = (float)cnv_size.get_width(); + float h = (float)cnv_size.get_height(); + + return { w - thickness_bar_width(canvas), 0.0f, w, h }; +} + +#if !ENABLE_LEGACY_OPENGL_REMOVAL +Rect GLCanvas3D::LayersEditing::get_bar_rect_viewport(const GLCanvas3D& canvas) +{ + const Size& cnv_size = canvas.get_canvas_size(); + float half_w = 0.5f * (float)cnv_size.get_width(); + float half_h = 0.5f * (float)cnv_size.get_height(); + float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom(); + return { (half_w - thickness_bar_width(canvas)) * inv_zoom, half_h * inv_zoom, half_w * inv_zoom, -half_h * inv_zoom }; +} +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + +bool GLCanvas3D::LayersEditing::is_initialized() const +{ + return wxGetApp().get_shader("variable_layer_height") != nullptr; +} + +std::string GLCanvas3D::LayersEditing::get_tooltip(const GLCanvas3D& canvas) const +{ + std::string ret; + if (m_enabled && m_layer_height_profile.size() >= 4) { + float z = get_cursor_z_relative(canvas); + if (z != -1000.0f) { + z *= m_object_max_z; + + float h = 0.0f; + for (size_t i = m_layer_height_profile.size() - 2; i >= 2; i -= 2) { + const float zi = static_cast(m_layer_height_profile[i]); + const float zi_1 = static_cast(m_layer_height_profile[i - 2]); + if (zi_1 <= z && z <= zi) { + float dz = zi - zi_1; + h = (dz != 0.0f) ? static_cast(lerp(m_layer_height_profile[i - 1], m_layer_height_profile[i + 1], (z - zi_1) / dz)) : + static_cast(m_layer_height_profile[i + 1]); + break; + } + } + if (h > 0.0f) + ret = std::to_string(h); + } + } + return ret; +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3D& canvas) +{ + const Size cnv_size = canvas.get_canvas_size(); + const float cnv_width = (float)cnv_size.get_width(); + const float cnv_height = (float)cnv_size.get_height(); + if (cnv_width == 0.0f || cnv_height == 0.0f) + return; + + const float cnv_inv_width = 1.0f / cnv_width; +#else +void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3D& canvas, const Rect& bar_rect) +{ +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader("variable_layer_height"); + if (shader == nullptr) + return; + + shader->start_using(); + + shader->set_uniform("z_to_texture_row", float(m_layers_texture.cells - 1) / (float(m_layers_texture.width) * m_object_max_z)); + shader->set_uniform("z_texture_row_to_normalized", 1.0f / (float)m_layers_texture.height); + shader->set_uniform("z_cursor", m_object_max_z * this->get_cursor_z_relative(canvas)); + shader->set_uniform("z_cursor_band_width", band_width); + shader->set_uniform("object_max_z", m_object_max_z); +#if ENABLE_LEGACY_OPENGL_REMOVAL + shader->set_uniform("view_model_matrix", Transform3d::Identity()); + shader->set_uniform("projection_matrix", Transform3d::Identity()); + shader->set_uniform("view_normal_matrix", (Matrix3d)Matrix3d::Identity()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); + glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id)); + + // Render the color bar +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!m_profile.background.is_initialized() || m_profile.old_canvas_width != cnv_width) { + m_profile.old_canvas_width = cnv_width; + m_profile.background.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3T2 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(6); + + // vertices + const float l = 1.0f - 2.0f * THICKNESS_BAR_WIDTH * cnv_inv_width; + const float r = 1.0f; + const float t = 1.0f; + const float b = -1.0f; + init_data.add_vertex(Vec3f(l, b, 0.0f), Vec3f::UnitZ(), Vec2f(0.0f, 0.0f)); + init_data.add_vertex(Vec3f(r, b, 0.0f), Vec3f::UnitZ(), Vec2f(1.0f, 0.0f)); + init_data.add_vertex(Vec3f(r, t, 0.0f), Vec3f::UnitZ(), Vec2f(1.0f, 1.0f)); + init_data.add_vertex(Vec3f(l, t, 0.0f), Vec3f::UnitZ(), Vec2f(0.0f, 1.0f)); + + // indices + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(2, 3, 0); + + m_profile.background.init_from(std::move(init_data)); + } + + m_profile.background.render(); +#else + const float l = bar_rect.get_left(); + const float r = bar_rect.get_right(); + const float t = bar_rect.get_top(); + const float b = bar_rect.get_bottom(); + + ::glBegin(GL_QUADS); + ::glNormal3f(0.0f, 0.0f, 1.0f); + ::glTexCoord2f(0.0f, 0.0f); ::glVertex2f(l, b); + ::glTexCoord2f(1.0f, 0.0f); ::glVertex2f(r, b); + ::glTexCoord2f(1.0f, 1.0f); ::glVertex2f(r, t); + ::glTexCoord2f(0.0f, 1.0f); ::glVertex2f(l, t); + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + + shader->stop_using(); +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLCanvas3D::LayersEditing::render_profile(const GLCanvas3D& canvas) +#else +void GLCanvas3D::LayersEditing::render_profile(const Rect& bar_rect) +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ + //FIXME show some kind of legend. + + if (!m_slicing_parameters) + return; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Size cnv_size = canvas.get_canvas_size(); + const float cnv_width = (float)cnv_size.get_width(); + const float cnv_height = (float)cnv_size.get_height(); + if (cnv_width == 0.0f || cnv_height == 0.0f) + return; + + // Make the vertical bar a bit wider so the layer height curve does not touch the edge of the bar region. + const float scale_x = THICKNESS_BAR_WIDTH / float(1.12 * m_slicing_parameters->max_layer_height); + const float scale_y = cnv_height / m_object_max_z; + + const float cnv_inv_width = 1.0f / cnv_width; + const float cnv_inv_height = 1.0f / cnv_height; +#else + // Make the vertical bar a bit wider so the layer height curve does not touch the edge of the bar region. + const float scale_x = bar_rect.get_width() / float(1.12 * m_slicing_parameters->max_layer_height); + const float scale_y = bar_rect.get_height() / m_object_max_z; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_LEGACY_OPENGL_REMOVAL + // Baseline + if (!m_profile.baseline.is_initialized() || m_profile.old_layer_height_profile != m_layer_height_profile) { + m_profile.baseline.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P2 }; + init_data.color = ColorRGBA::BLACK(); + init_data.reserve_vertices(2); + init_data.reserve_indices(2); + + // vertices + const float axis_x = 2.0f * ((cnv_width - THICKNESS_BAR_WIDTH + float(m_slicing_parameters->layer_height) * scale_x) * cnv_inv_width - 0.5f); + init_data.add_vertex(Vec2f(axis_x, -1.0f)); + init_data.add_vertex(Vec2f(axis_x, 1.0f)); + + // indices + init_data.add_line(0, 1); + + m_profile.baseline.init_from(std::move(init_data)); + } + + if (!m_profile.profile.is_initialized() || m_profile.old_layer_height_profile != m_layer_height_profile) { + m_profile.old_layer_height_profile = m_layer_height_profile; + m_profile.profile.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::LineStrip, GLModel::Geometry::EVertexLayout::P2 }; + init_data.color = ColorRGBA::BLUE(); + init_data.reserve_vertices(m_layer_height_profile.size() / 2); + init_data.reserve_indices(m_layer_height_profile.size() / 2); + + // vertices + indices + for (unsigned int i = 0; i < (unsigned int)m_layer_height_profile.size(); i += 2) { + init_data.add_vertex(Vec2f(2.0f * ((cnv_width - THICKNESS_BAR_WIDTH + float(m_layer_height_profile[i + 1]) * scale_x) * cnv_inv_width - 0.5f), + 2.0f * (float(m_layer_height_profile[i]) * scale_y * cnv_inv_height - 0.5))); + init_data.add_index(i / 2); + } + + m_profile.profile.init_from(std::move(init_data)); + } + +#if ENABLE_GL_CORE_PROFILE + GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); +#else + GLShaderProgram* shader = wxGetApp().get_shader("flat"); +#endif // ENABLE_GL_CORE_PROFILE + if (shader != nullptr) { + shader->start_using(); + shader->set_uniform("view_model_matrix", Transform3d::Identity()); + shader->set_uniform("projection_matrix", Transform3d::Identity()); +#if ENABLE_GL_CORE_PROFILE + const std::array& viewport = wxGetApp().plater()->get_camera().get_viewport(); + shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); + shader->set_uniform("width", 0.25f); + shader->set_uniform("gap_size", 0.0f); +#endif // ENABLE_GL_CORE_PROFILE + m_profile.baseline.render(); + m_profile.profile.render(); + shader->stop_using(); + } +#else + const float x = bar_rect.get_left() + float(m_slicing_parameters->layer_height) * scale_x; + + // Baseline + glsafe(::glColor3f(0.0f, 0.0f, 0.0f)); + ::glBegin(GL_LINE_STRIP); + ::glVertex2f(x, bar_rect.get_bottom()); + ::glVertex2f(x, bar_rect.get_top()); + glsafe(::glEnd()); + + // Curve + glsafe(::glColor3f(0.0f, 0.0f, 1.0f)); + ::glBegin(GL_LINE_STRIP); + for (unsigned int i = 0; i < m_layer_height_profile.size(); i += 2) + ::glVertex2f(bar_rect.get_left() + (float)m_layer_height_profile[i + 1] * scale_x, bar_rect.get_bottom() + (float)m_layer_height_profile[i] * scale_y); + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +void GLCanvas3D::LayersEditing::render_volumes(const GLCanvas3D& canvas, const GLVolumeCollection& volumes) +{ + assert(this->is_allowed()); + assert(this->last_object_id != -1); + + GLShaderProgram* current_shader = wxGetApp().get_current_shader(); + ScopeGuard guard([current_shader]() { if (current_shader != nullptr) current_shader->start_using(); }); + if (current_shader != nullptr) + current_shader->stop_using(); + + GLShaderProgram* shader = wxGetApp().get_shader("variable_layer_height"); + if (shader == nullptr) + return; + + shader->start_using(); + + generate_layer_height_texture(); + + // Uniforms were resolved, go ahead using the layer editing shader. + shader->set_uniform("z_to_texture_row", float(m_layers_texture.cells - 1) / (float(m_layers_texture.width) * float(m_object_max_z))); + shader->set_uniform("z_texture_row_to_normalized", 1.0f / float(m_layers_texture.height)); + shader->set_uniform("z_cursor", float(m_object_max_z) * float(this->get_cursor_z_relative(canvas))); + shader->set_uniform("z_cursor_band_width", float(this->band_width)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + // Initialize the layer height texture mapping. + const GLsizei w = (GLsizei)m_layers_texture.width; + const GLsizei h = (GLsizei)m_layers_texture.height; + const GLsizei half_w = w / 2; + const GLsizei half_h = h / 2; + glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); + glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, half_w, half_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0)); + glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data())); + glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, half_w, half_h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data() + m_layers_texture.width * m_layers_texture.height * 4)); + for (GLVolume* glvolume : volumes.volumes) { + // Render the object using the layer editing shader and texture. + if (!glvolume->is_active || glvolume->composite_id.object_id != this->last_object_id || glvolume->is_modifier) + continue; + + shader->set_uniform("volume_world_matrix", glvolume->world_matrix()); + shader->set_uniform("object_max_z", 0.0f); +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Transform3d& view_matrix = camera.get_view_matrix(); + const Transform3d model_matrix = glvolume->world_matrix(); + shader->set_uniform("view_model_matrix", view_matrix * model_matrix); + const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader->set_uniform("view_normal_matrix", view_normal_matrix); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + glvolume->render(); + } + // Revert back to the previous shader. + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); +} + +void GLCanvas3D::LayersEditing::adjust_layer_height_profile() +{ + this->update_slicing_parameters(); + PrintObject::update_layer_height_profile(*m_model_object, *m_slicing_parameters, m_layer_height_profile); + Slic3r::adjust_layer_height_profile(*m_slicing_parameters, m_layer_height_profile, this->last_z, this->strength, this->band_width, this->last_action); + m_layer_height_profile_modified = true; + m_layers_texture.valid = false; +} + +void GLCanvas3D::LayersEditing::reset_layer_height_profile(GLCanvas3D& canvas) +{ + const_cast(m_model_object)->layer_height_profile.clear(); + m_layer_height_profile.clear(); + m_layers_texture.valid = false; + canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + wxGetApp().obj_list()->update_info_items(last_object_id); +} + +void GLCanvas3D::LayersEditing::adaptive_layer_height_profile(GLCanvas3D& canvas, float quality_factor) +{ + this->update_slicing_parameters(); + m_layer_height_profile = layer_height_profile_adaptive(*m_slicing_parameters, *m_model_object, quality_factor); + const_cast(m_model_object)->layer_height_profile.set(m_layer_height_profile); + m_layers_texture.valid = false; + canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + wxGetApp().obj_list()->update_info_items(last_object_id); +} + +void GLCanvas3D::LayersEditing::smooth_layer_height_profile(GLCanvas3D& canvas, const HeightProfileSmoothingParams& smoothing_params) +{ + this->update_slicing_parameters(); + m_layer_height_profile = smooth_height_profile(m_layer_height_profile, *m_slicing_parameters, smoothing_params); + const_cast(m_model_object)->layer_height_profile.set(m_layer_height_profile); + m_layers_texture.valid = false; + canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + wxGetApp().obj_list()->update_info_items(last_object_id); +} + +void GLCanvas3D::LayersEditing::generate_layer_height_texture() +{ + this->update_slicing_parameters(); + // Always try to update the layer height profile. + bool update = ! m_layers_texture.valid; + if (PrintObject::update_layer_height_profile(*m_model_object, *m_slicing_parameters, m_layer_height_profile)) { + // Initialized to the default value. + m_layer_height_profile_modified = false; + update = true; + } + // Update if the layer height profile was changed, or when the texture is not valid. + if (! update && ! m_layers_texture.data.empty() && m_layers_texture.cells > 0) + // Texture is valid, don't update. + return; + + if (m_layers_texture.data.empty()) { + m_layers_texture.width = 1024; + m_layers_texture.height = 1024; + m_layers_texture.levels = 2; + m_layers_texture.data.assign(m_layers_texture.width * m_layers_texture.height * 5, 0); + } + + bool level_of_detail_2nd_level = true; + m_layers_texture.cells = Slic3r::generate_layer_height_texture( + *m_slicing_parameters, + Slic3r::generate_object_layers(*m_slicing_parameters, m_layer_height_profile), + m_layers_texture.data.data(), m_layers_texture.height, m_layers_texture.width, level_of_detail_2nd_level); + m_layers_texture.valid = true; +} + +void GLCanvas3D::LayersEditing::accept_changes(GLCanvas3D& canvas) +{ + if (last_object_id >= 0) { + if (m_layer_height_profile_modified) { + wxGetApp().plater()->take_snapshot(_L("Variable layer height - Manual edit")); + const_cast(m_model_object)->layer_height_profile.set(m_layer_height_profile); + canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + wxGetApp().obj_list()->update_info_items(last_object_id); + } + } + m_layer_height_profile_modified = false; +} + +void GLCanvas3D::LayersEditing::update_slicing_parameters() +{ + if (m_slicing_parameters == nullptr) { + m_slicing_parameters = new SlicingParameters(); + *m_slicing_parameters = PrintObject::slicing_parameters(*m_config, *m_model_object, m_object_max_z); + } +} + +float GLCanvas3D::LayersEditing::thickness_bar_width(const GLCanvas3D &canvas) +{ + return +#if ENABLE_RETINA_GL + canvas.get_canvas_size().get_scale_factor() +#else + canvas.get_wxglcanvas()->GetContentScaleFactor() +#endif + * THICKNESS_BAR_WIDTH; +} + + +const Point GLCanvas3D::Mouse::Drag::Invalid_2D_Point(INT_MAX, INT_MAX); +const Vec3d GLCanvas3D::Mouse::Drag::Invalid_3D_Point(DBL_MAX, DBL_MAX, DBL_MAX); +const int GLCanvas3D::Mouse::Drag::MoveThresholdPx = 5; + +GLCanvas3D::Mouse::Drag::Drag() + : start_position_2D(Invalid_2D_Point) + , start_position_3D(Invalid_3D_Point) + , move_volume_idx(-1) + , move_requires_threshold(false) + , move_start_threshold_position_2D(Invalid_2D_Point) +{ +} + +GLCanvas3D::Mouse::Mouse() + : dragging(false) + , position(DBL_MAX, DBL_MAX) + , scene_position(DBL_MAX, DBL_MAX, DBL_MAX) + , ignore_left_up(false) +{ +} + +void GLCanvas3D::Labels::render(const std::vector& sorted_instances) const +{ + if (!m_enabled || !is_shown()) + return; + + const Camera& camera = wxGetApp().plater()->get_camera(); + const Model* model = m_canvas.get_model(); + if (model == nullptr) + return; + + Transform3d world_to_eye = camera.get_view_matrix(); + Transform3d world_to_screen = camera.get_projection_matrix() * world_to_eye; + const std::array& viewport = camera.get_viewport(); + + struct Owner + { + int obj_idx; + int inst_idx; + size_t model_instance_id; + BoundingBoxf3 world_box; + double eye_center_z; + std::string title; + std::string label; + std::string print_order; + bool selected; + }; + + // collect owners world bounding boxes and data from volumes + std::vector owners; + const GLVolumeCollection& volumes = m_canvas.get_volumes(); + for (const GLVolume* volume : volumes.volumes) { + int obj_idx = volume->object_idx(); + if (0 <= obj_idx && obj_idx < (int)model->objects.size()) { + int inst_idx = volume->instance_idx(); + std::vector::iterator it = std::find_if(owners.begin(), owners.end(), [obj_idx, inst_idx](const Owner& owner) { + return (owner.obj_idx == obj_idx) && (owner.inst_idx == inst_idx); + }); + if (it != owners.end()) { + it->world_box.merge(volume->transformed_bounding_box()); + it->selected &= volume->selected; + } else { + const ModelObject* model_object = model->objects[obj_idx]; + Owner owner; + owner.obj_idx = obj_idx; + owner.inst_idx = inst_idx; + owner.model_instance_id = model_object->instances[inst_idx]->id().id; + owner.world_box = volume->transformed_bounding_box(); + owner.title = "object" + std::to_string(obj_idx) + "_inst##" + std::to_string(inst_idx); + owner.label = model_object->name; + if (model_object->instances.size() > 1) + owner.label += " (" + std::to_string(inst_idx + 1) + ")"; + owner.selected = volume->selected; + owners.emplace_back(owner); + } + } + } + + // updates print order strings + if (sorted_instances.size() > 1) { + for (size_t i = 0; i < sorted_instances.size(); ++i) { + size_t id = sorted_instances[i]->id().id; + std::vector::iterator it = std::find_if(owners.begin(), owners.end(), [id](const Owner& owner) { + return owner.model_instance_id == id; + }); + if (it != owners.end()) + it->print_order = std::string((_(L("Seq."))).ToUTF8()) + "#: " + std::to_string(i + 1); + } + } + + // calculate eye bounding boxes center zs + for (Owner& owner : owners) { + owner.eye_center_z = (world_to_eye * owner.world_box.center())(2); + } + + // sort owners by center eye zs and selection + std::sort(owners.begin(), owners.end(), [](const Owner& owner1, const Owner& owner2) { + if (!owner1.selected && owner2.selected) + return true; + else if (owner1.selected && !owner2.selected) + return false; + else + return (owner1.eye_center_z < owner2.eye_center_z); + }); + + ImGuiWrapper& imgui = *wxGetApp().imgui(); + + // render info windows + for (const Owner& owner : owners) { + Vec3d screen_box_center = world_to_screen * owner.world_box.center(); + float x = 0.0f; + float y = 0.0f; + if (camera.get_type() == Camera::EType::Perspective) { + x = (0.5f + 0.001f * 0.5f * (float)screen_box_center(0)) * viewport[2]; + y = (0.5f - 0.001f * 0.5f * (float)screen_box_center(1)) * viewport[3]; + } else { + x = (0.5f + 0.5f * (float)screen_box_center(0)) * viewport[2]; + y = (0.5f - 0.5f * (float)screen_box_center(1)) * viewport[3]; + } + + if (x < 0.0f || viewport[2] < x || y < 0.0f || viewport[3] < y) + continue; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, owner.selected ? 3.0f : 1.5f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleColor(ImGuiCol_Border, owner.selected ? ImVec4(0.757f, 0.404f, 0.216f, 1.0f) : ImVec4(0.75f, 0.75f, 0.75f, 1.0f)); + imgui.set_next_window_pos(x, y, ImGuiCond_Always, 0.5f, 0.5f); + imgui.begin(owner.title, ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove); + ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); + float win_w = ImGui::GetWindowWidth(); + float label_len = ImGui::CalcTextSize(owner.label.c_str()).x; + ImGui::SetCursorPosX(0.5f * (win_w - label_len)); + ImGui::AlignTextToFramePadding(); + imgui.text(owner.label); + + if (!owner.print_order.empty()) { + ImGui::Separator(); + float po_len = ImGui::CalcTextSize(owner.print_order.c_str()).x; + ImGui::SetCursorPosX(0.5f * (win_w - po_len)); + ImGui::AlignTextToFramePadding(); + imgui.text(owner.print_order); + } + + // force re-render while the windows gets to its final size (it takes several frames) + if (ImGui::GetWindowContentRegionWidth() + 2.0f * ImGui::GetStyle().WindowPadding.x != ImGui::CalcWindowNextAutoFitSize(ImGui::GetCurrentWindow()).x) + imgui.set_requires_extra_frame(); + + imgui.end(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(2); + } +} + +void GLCanvas3D::Tooltip::set_text(const std::string& text) +{ + // If the mouse is inside an ImGUI dialog, then the tooltip is suppressed. + m_text = m_in_imgui ? std::string() : text; +} + +void GLCanvas3D::Tooltip::render(const Vec2d& mouse_position, GLCanvas3D& canvas) +{ + static ImVec2 size(0.0f, 0.0f); + + auto validate_position = [](const Vec2d& position, const GLCanvas3D& canvas, const ImVec2& wnd_size) { + auto calc_cursor_height = []() { + float ret = 16.0f; +#ifdef _WIN32 + // see: https://forums.codeguru.com/showthread.php?449040-get-the-system-current-cursor-size + // this code is not perfect because it returns a maximum height equal to 31 even if the cursor bitmap shown on screen is bigger + // but at least it gives the same result as wxWidgets in the settings tabs + ICONINFO ii; + if (::GetIconInfo((HICON)GetCursor(), &ii) != 0) { + BITMAP bitmap; + ::GetObject(ii.hbmMask, sizeof(BITMAP), &bitmap); + int width = bitmap.bmWidth; + int height = (ii.hbmColor == nullptr) ? bitmap.bmHeight / 2 : bitmap.bmHeight; + HDC dc = ::CreateCompatibleDC(nullptr); + if (dc != nullptr) { + if (::SelectObject(dc, ii.hbmMask) != nullptr) { + for (int i = 0; i < width; ++i) { + for (int j = 0; j < height; ++j) { + if (::GetPixel(dc, i, j) != RGB(255, 255, 255)) { + if (ret < float(j)) + ret = float(j); + } + } + } + ::DeleteDC(dc); + } + } + ::DeleteObject(ii.hbmColor); + ::DeleteObject(ii.hbmMask); + } +#endif // _WIN32 + return ret; + }; + + const Size cnv_size = canvas.get_canvas_size(); + const float x = std::clamp(float(position.x()), 0.0f, float(cnv_size.get_width()) - wnd_size.x); + const float y = std::clamp(float(position.y()) + calc_cursor_height(), 0.0f, float(cnv_size.get_height()) - wnd_size.y); + return Vec2f(x, y); + }; + + if (m_text.empty()) { + m_start_time = std::chrono::steady_clock::now(); + return; + } + + // draw the tooltip as hidden until the delay is expired + // use a value of alpha slightly different from 0.0f because newer imgui does not calculate properly the window size if alpha == 0.0f + const float alpha = (std::chrono::duration_cast(std::chrono::steady_clock::now() - m_start_time).count() < 500) ? 0.01f : 1.0f; + + const Vec2f position = validate_position(mouse_position, canvas, size); + + ImGuiWrapper& imgui = *wxGetApp().imgui(); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha); + imgui.set_next_window_pos(position.x(), position.y(), ImGuiCond_Always, 0.0f, 0.0f); + + imgui.begin(wxString("canvas_tooltip"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoFocusOnAppearing); + ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); + ImGui::TextUnformatted(m_text.c_str()); + + // force re-render while the windows gets to its final size (it may take several frames) or while hidden + if (alpha < 1.0f || ImGui::GetWindowContentRegionWidth() + 2.0f * ImGui::GetStyle().WindowPadding.x != ImGui::CalcWindowNextAutoFitSize(ImGui::GetCurrentWindow()).x) + imgui.set_requires_extra_frame(); + + size = ImGui::GetWindowSize(); + + imgui.end(); + ImGui::PopStyleVar(2); +} + +void GLCanvas3D::SequentialPrintClearance::set_polygons(const Polygons& polygons) +{ + m_perimeter.reset(); + m_fill.reset(); + if (polygons.empty()) + return; + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + size_t triangles_count = 0; + for (const Polygon& poly : polygons) { + triangles_count += poly.points.size() - 2; + } + const size_t vertices_count = 3 * triangles_count; +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + if (m_render_fill) { + GLModel::Geometry fill_data; +#if ENABLE_LEGACY_OPENGL_REMOVAL + fill_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + fill_data.color = { 0.3333f, 0.0f, 0.0f, 0.5f }; + + // vertices + indices + const ExPolygons polygons_union = union_ex(polygons); + unsigned int vertices_counter = 0; + for (const ExPolygon& poly : polygons_union) { + const std::vector triangulation = triangulate_expolygon_3d(poly); + fill_data.reserve_vertices(fill_data.vertices_count() + triangulation.size()); + fill_data.reserve_indices(fill_data.indices_count() + triangulation.size()); + for (const Vec3d& v : triangulation) { + fill_data.add_vertex((Vec3f)(v.cast() + 0.0125f * Vec3f::UnitZ())); // add a small positive z to avoid z-fighting + ++vertices_counter; + if (vertices_counter % 3 == 0) + fill_data.add_triangle(vertices_counter - 3, vertices_counter - 2, vertices_counter - 1); + } + } + + m_fill.init_from(std::move(fill_data)); +#else + GLModel::Geometry::Entity entity; + entity.type = GLModel::EPrimitiveType::Triangles; + entity.color = { 0.3333f, 0.0f, 0.0f, 0.5f }; + entity.positions.reserve(vertices_count); + entity.normals.reserve(vertices_count); + entity.indices.reserve(vertices_count); + + const ExPolygons polygons_union = union_ex(polygons); + for (const ExPolygon& poly : polygons_union) { + const std::vector triangulation = triangulate_expolygon_3d(poly); + for (const Vec3d& v : triangulation) { + entity.positions.emplace_back(v.cast() + Vec3f(0.0f, 0.0f, 0.0125f)); // add a small positive z to avoid z-fighting + entity.normals.emplace_back(Vec3f::UnitZ()); + const size_t positions_count = entity.positions.size(); + if (positions_count % 3 == 0) { + entity.indices.emplace_back(positions_count - 3); + entity.indices.emplace_back(positions_count - 2); + entity.indices.emplace_back(positions_count - 1); + } + } + } + + fill_data.entities.emplace_back(entity); + m_fill.init_from(fill_data); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_perimeter.init_from(polygons, 0.025f); // add a small positive z to avoid z-fighting +#else + GLModel::Geometry perimeter_data; + for (const Polygon& poly : polygons) { + GLModel::Geometry::Entity ent; + ent.type = GLModel::EPrimitiveType::LineLoop; + ent.positions.reserve(poly.points.size()); + ent.indices.reserve(poly.points.size()); + unsigned int id_count = 0; + for (const Point& p : poly.points) { + ent.positions.emplace_back(unscale(p.x()), unscale(p.y()), 0.025f); // add a small positive z to avoid z-fighting + ent.normals.emplace_back(Vec3f::UnitZ()); + ent.indices.emplace_back(id_count++); + } + + perimeter_data.entities.emplace_back(ent); + } + + m_perimeter.init_from(perimeter_data); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +void GLCanvas3D::SequentialPrintClearance::render() +{ + const ColorRGBA FILL_COLOR = { 1.0f, 0.0f, 0.0f, 0.5f }; + const ColorRGBA NO_FILL_COLOR = { 1.0f, 1.0f, 1.0f, 0.75f }; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader("flat"); +#else + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + if (shader == nullptr) + return; + + shader->start_using(); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix()); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glDisable(GL_CULL_FACE)); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_perimeter.set_color(m_render_fill ? FILL_COLOR : NO_FILL_COLOR); +#else + m_perimeter.set_color(-1, m_render_fill ? FILL_COLOR : NO_FILL_COLOR); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + m_perimeter.render(); + m_fill.render(); + + glsafe(::glDisable(GL_BLEND)); + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glDisable(GL_DEPTH_TEST)); + + shader->stop_using(); +} + +wxDEFINE_EVENT(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_OBJECT_SELECT, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_RIGHT_CLICK, RBtnEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_REMOVE_OBJECT, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_ARRANGE, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event); +wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent); +#if ENABLE_WORLD_COORDINATE +wxDEFINE_EVENT(EVT_GLCANVAS_RESET_SKEW, SimpleEvent); +#endif // ENABLE_WORLD_COORDINATE +wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3dEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, Event); +wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_GEOMETRY, Vec3dsEvent<2>); +wxDEFINE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_MOVE_SLIDERS, wxKeyEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_EDIT_COLOR_CHANGE, wxKeyEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_JUMP_TO, wxKeyEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_COLLAPSE_SIDEBAR, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, Event); +wxDEFINE_EVENT(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, HeightProfileSmoothEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_RELOAD_FROM_DISK, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_RENDER_TIMER, wxTimerEvent/*RenderTimerEvent*/); +wxDEFINE_EVENT(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, wxTimerEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, wxTimerEvent); + +const double GLCanvas3D::DefaultCameraZoomToBoxMarginFactor = 1.25; + +void GLCanvas3D::load_arrange_settings() +{ + std::string dist_fff_str = + wxGetApp().app_config->get("arrange", "min_object_distance_fff"); + + std::string dist_bed_fff_str = + wxGetApp().app_config->get("arrange", "min_bed_distance_fff"); + + std::string dist_fff_seq_print_str = + wxGetApp().app_config->get("arrange", "min_object_distance_fff_seq_print"); + + std::string dist_bed_fff_seq_print_str = + wxGetApp().app_config->get("arrange", "min_bed_distance_fff_seq_print"); + + std::string dist_sla_str = + wxGetApp().app_config->get("arrange", "min_object_distance_sla"); + + std::string dist_bed_sla_str = + wxGetApp().app_config->get("arrange", "min_bed_distance_sla"); + + std::string en_rot_fff_str = + wxGetApp().app_config->get("arrange", "enable_rotation_fff"); + + std::string en_rot_fff_seqp_str = + wxGetApp().app_config->get("arrange", "enable_rotation_fff_seq_print"); + + std::string en_rot_sla_str = + wxGetApp().app_config->get("arrange", "enable_rotation_sla"); + + if (!dist_fff_str.empty()) + m_arrange_settings_fff.distance = string_to_float_decimal_point(dist_fff_str); + + if (!dist_bed_fff_str.empty()) + m_arrange_settings_fff.distance_from_bed = string_to_float_decimal_point(dist_bed_fff_str); + + if (!dist_fff_seq_print_str.empty()) + m_arrange_settings_fff_seq_print.distance = string_to_float_decimal_point(dist_fff_seq_print_str); + + if (!dist_bed_fff_seq_print_str.empty()) + m_arrange_settings_fff_seq_print.distance_from_bed = string_to_float_decimal_point(dist_bed_fff_seq_print_str); + + if (!dist_sla_str.empty()) + m_arrange_settings_sla.distance = string_to_float_decimal_point(dist_sla_str); + + if (!dist_bed_sla_str.empty()) + m_arrange_settings_sla.distance_from_bed = string_to_float_decimal_point(dist_bed_sla_str); + + if (!en_rot_fff_str.empty()) + m_arrange_settings_fff.enable_rotation = (en_rot_fff_str == "1" || en_rot_fff_str == "yes"); + + if (!en_rot_fff_seqp_str.empty()) + m_arrange_settings_fff_seq_print.enable_rotation = (en_rot_fff_seqp_str == "1" || en_rot_fff_seqp_str == "yes"); + + if (!en_rot_sla_str.empty()) + m_arrange_settings_sla.enable_rotation = (en_rot_sla_str == "1" || en_rot_sla_str == "yes"); +} + +PrinterTechnology GLCanvas3D::current_printer_technology() const +{ + return m_process->current_printer_technology(); +} + +GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed) + : m_canvas(canvas) + , m_context(nullptr) + , m_bed(bed) +#if ENABLE_RETINA_GL + , m_retina_helper(nullptr) +#endif + , m_in_render(false) + , m_main_toolbar(GLToolbar::Normal, "Main") + , m_undoredo_toolbar(GLToolbar::Normal, "Undo_Redo") + , m_gizmos(*this) + , m_use_clipping_planes(false) + , m_sidebar_field("") + , m_extra_frame_requested(false) + , m_config(nullptr) + , m_process(nullptr) + , m_model(nullptr) + , m_dirty(true) + , m_initialized(false) + , m_apply_zoom_to_volumes_filter(false) + , m_picking_enabled(false) + , m_moving_enabled(false) + , m_dynamic_background_enabled(false) + , m_multisample_allowed(false) + , m_moving(false) + , m_tab_down(false) + , m_cursor_type(Standard) + , m_reload_delayed(false) + , m_render_sla_auxiliaries(true) + , m_labels(*this) + , m_slope(m_volumes) +{ + if (m_canvas != nullptr) { + m_timer.SetOwner(m_canvas); + m_render_timer.SetOwner(m_canvas); +#if ENABLE_RETINA_GL + m_retina_helper.reset(new RetinaHelper(canvas)); +#endif // ENABLE_RETINA_GL + } + + load_arrange_settings(); + + m_selection.set_volumes(&m_volumes.volumes); +} + +GLCanvas3D::~GLCanvas3D() +{ + reset_volumes(); +} + +void GLCanvas3D::post_event(wxEvent &&event) +{ + event.SetEventObject(m_canvas); + wxPostEvent(m_canvas, event); +} + +bool GLCanvas3D::init() +{ + if (m_initialized) + return true; + + if (m_canvas == nullptr || m_context == nullptr) + return false; + + glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 1.0f)); +#if ENABLE_OPENGL_ES + glsafe(::glClearDepthf(1.0f)); +#else + glsafe(::glClearDepth(1.0f)); +#endif // ENABLE_OPENGL_ES + + glsafe(::glDepthFunc(GL_LESS)); + + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + // Set antialiasing / multisampling + glsafe(::glDisable(GL_LINE_SMOOTH)); + glsafe(::glDisable(GL_POLYGON_SMOOTH)); + + // ambient lighting + GLfloat ambient[4] = { 0.3f, 0.3f, 0.3f, 1.0f }; + glsafe(::glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient)); + + glsafe(::glEnable(GL_LIGHT0)); + glsafe(::glEnable(GL_LIGHT1)); + + // light from camera + GLfloat specular_cam[4] = { 0.3f, 0.3f, 0.3f, 1.0f }; + glsafe(::glLightfv(GL_LIGHT1, GL_SPECULAR, specular_cam)); + GLfloat diffuse_cam[4] = { 0.2f, 0.2f, 0.2f, 1.0f }; + glsafe(::glLightfv(GL_LIGHT1, GL_DIFFUSE, diffuse_cam)); + + // light from above + GLfloat specular_top[4] = { 0.2f, 0.2f, 0.2f, 1.0f }; + glsafe(::glLightfv(GL_LIGHT0, GL_SPECULAR, specular_top)); + GLfloat diffuse_top[4] = { 0.5f, 0.5f, 0.5f, 1.0f }; + glsafe(::glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse_top)); + + // Enables Smooth Color Shading; try GL_FLAT for (lack of) fun. + glsafe(::glShadeModel(GL_SMOOTH)); + + // A handy trick -- have surface material mirror the color. + glsafe(::glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)); + glsafe(::glEnable(GL_COLOR_MATERIAL)); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + if (m_multisample_allowed) + glsafe(::glEnable(GL_MULTISAMPLE)); + + if (m_main_toolbar.is_enabled()) + m_layers_editing.init(); + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + // on linux the gl context is not valid until the canvas is not shown on screen + // we defer the geometry finalization of volumes until the first call to render() + m_volumes.finalize_geometry(true); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + if (m_gizmos.is_enabled() && !m_gizmos.init()) + std::cout << "Unable to initialize gizmos: please, check that all the required textures are available" << std::endl; + + if (!_init_toolbars()) + return false; + + if (m_selection.is_enabled() && !m_selection.init()) + return false; + + m_initialized = true; + + return true; +} + +void GLCanvas3D::set_as_dirty() +{ + m_dirty = true; +} + +unsigned int GLCanvas3D::get_volumes_count() const +{ + return (unsigned int)m_volumes.volumes.size(); +} + +void GLCanvas3D::reset_volumes() +{ + if (!m_initialized) + return; + + if (m_volumes.empty()) + return; + + _set_current(); + + m_selection.clear(); + m_volumes.clear(); + m_dirty = true; + + _set_warning_notification(EWarning::ObjectOutside, false); +} + +ModelInstanceEPrintVolumeState GLCanvas3D::check_volumes_outside_state() const +{ + ModelInstanceEPrintVolumeState state = ModelInstanceEPrintVolumeState::ModelInstancePVS_Inside; + if (m_initialized) + m_volumes.check_outside_state(m_bed.build_volume(), &state); + return state; +} + +void GLCanvas3D::toggle_sla_auxiliaries_visibility(bool visible, const ModelObject* mo, int instance_idx) +{ + if (current_printer_technology() != ptSLA) + return; + + m_render_sla_auxiliaries = visible; + + for (GLVolume* vol : m_volumes.volumes) { + if ((mo == nullptr || m_model->objects[vol->composite_id.object_id] == mo) + && (instance_idx == -1 || vol->composite_id.instance_id == instance_idx) + && vol->composite_id.volume_id < 0) + vol->is_active = visible; + } +} + +void GLCanvas3D::toggle_model_objects_visibility(bool visible, const ModelObject* mo, int instance_idx, const ModelVolume* mv) +{ +#if ENABLE_RAYCAST_PICKING + std::vector>* raycasters = get_raycasters_for_picking(SceneRaycaster::EType::Volume); +#endif // ENABLE_RAYCAST_PICKING + for (GLVolume* vol : m_volumes.volumes) { + if (vol->is_wipe_tower) + vol->is_active = (visible && mo == nullptr); + else { + if ((mo == nullptr || m_model->objects[vol->composite_id.object_id] == mo) + && (instance_idx == -1 || vol->composite_id.instance_id == instance_idx) + && (mv == nullptr || m_model->objects[vol->composite_id.object_id]->volumes[vol->composite_id.volume_id] == mv)) { + vol->is_active = visible; + if (!vol->is_modifier) + vol->color.a(1.f); + + if (instance_idx == -1) { + vol->force_native_color = false; + vol->force_neutral_color = false; + } else { + const GLGizmosManager& gm = get_gizmos_manager(); + auto gizmo_type = gm.get_current_type(); + if ( (gizmo_type == GLGizmosManager::FdmSupports + || gizmo_type == GLGizmosManager::Seam + || gizmo_type == GLGizmosManager::Cut) + && ! vol->is_modifier) { + vol->force_neutral_color = true; + if (gizmo_type == GLGizmosManager::Cut) + vol->color.a(0.95f); + } + else if (gizmo_type == GLGizmosManager::MmuSegmentation) + vol->is_active = false; + else + vol->force_native_color = true; + } + } + } +#if ENABLE_RAYCAST_PICKING + auto it = std::find_if(raycasters->begin(), raycasters->end(), [vol](std::shared_ptr item) { return item->get_raycaster() == vol->mesh_raycaster.get(); }); + if (it != raycasters->end()) + (*it)->set_active(vol->is_active); +#endif // ENABLE_RAYCAST_PICKING + } + + if (visible && !mo) + toggle_sla_auxiliaries_visibility(true, mo, instance_idx); + + if (!mo && !visible && !m_model->objects.empty() && (m_model->objects.size() > 1 || m_model->objects.front()->instances.size() > 1)) + _set_warning_notification(EWarning::SomethingNotShown, true); + + if (!mo && visible) + _set_warning_notification(EWarning::SomethingNotShown, false); +} + +void GLCanvas3D::update_instance_printable_state_for_object(const size_t obj_idx) +{ + ModelObject* model_object = m_model->objects[obj_idx]; + for (int inst_idx = 0; inst_idx < (int)model_object->instances.size(); ++inst_idx) { + ModelInstance* instance = model_object->instances[inst_idx]; + + for (GLVolume* volume : m_volumes.volumes) { + if (volume->object_idx() == (int)obj_idx && volume->instance_idx() == inst_idx) + volume->printable = instance->printable; + } + } +} + +void GLCanvas3D::update_instance_printable_state_for_objects(const std::vector& object_idxs) +{ + for (size_t obj_idx : object_idxs) + update_instance_printable_state_for_object(obj_idx); +} + +void GLCanvas3D::set_config(const DynamicPrintConfig* config) +{ + m_config = config; + m_layers_editing.set_config(config); +} + +void GLCanvas3D::set_process(BackgroundSlicingProcess *process) +{ + m_process = process; +} + +void GLCanvas3D::set_model(Model* model) +{ + m_model = model; + m_selection.set_model(m_model); +} + +void GLCanvas3D::bed_shape_changed() +{ + refresh_camera_scene_box(); + wxGetApp().plater()->get_camera().requires_zoom_to_bed = true; + m_dirty = true; +} + +void GLCanvas3D::refresh_camera_scene_box() +{ + wxGetApp().plater()->get_camera().set_scene_box(scene_bounding_box()); +} + +BoundingBoxf3 GLCanvas3D::volumes_bounding_box() const +{ + BoundingBoxf3 bb; + for (const GLVolume* volume : m_volumes.volumes) { + if (!m_apply_zoom_to_volumes_filter || ((volume != nullptr) && volume->zoom_to_volumes)) + bb.merge(volume->transformed_bounding_box()); + } + return bb; +} + +BoundingBoxf3 GLCanvas3D::scene_bounding_box() const +{ + BoundingBoxf3 bb = volumes_bounding_box(); + bb.merge(m_bed.extended_bounding_box()); + double h = m_bed.build_volume().max_print_height(); + //FIXME why -h? + bb.min.z() = std::min(bb.min.z(), -h); + bb.max.z() = std::max(bb.max.z(), h); + return bb; +} + +bool GLCanvas3D::is_layers_editing_enabled() const +{ + return m_layers_editing.is_enabled(); +} + +bool GLCanvas3D::is_layers_editing_allowed() const +{ + return m_layers_editing.is_allowed(); +} + +void GLCanvas3D::reset_layer_height_profile() +{ + wxGetApp().plater()->take_snapshot(_L("Variable layer height - Reset")); + m_layers_editing.reset_layer_height_profile(*this); + m_layers_editing.state = LayersEditing::Completed; + m_dirty = true; +} + +void GLCanvas3D::adaptive_layer_height_profile(float quality_factor) +{ + wxGetApp().plater()->take_snapshot(_L("Variable layer height - Adaptive")); + m_layers_editing.adaptive_layer_height_profile(*this, quality_factor); + m_layers_editing.state = LayersEditing::Completed; + m_dirty = true; +} + +void GLCanvas3D::smooth_layer_height_profile(const HeightProfileSmoothingParams& smoothing_params) +{ + wxGetApp().plater()->take_snapshot(_L("Variable layer height - Smooth all")); + m_layers_editing.smooth_layer_height_profile(*this, smoothing_params); + m_layers_editing.state = LayersEditing::Completed; + m_dirty = true; +} + +bool GLCanvas3D::is_reload_delayed() const +{ + return m_reload_delayed; +} + +void GLCanvas3D::enable_layers_editing(bool enable) +{ + m_layers_editing.set_enabled(enable); + set_as_dirty(); +} + +void GLCanvas3D::enable_legend_texture(bool enable) +{ + m_gcode_viewer.enable_legend(enable); +} + +void GLCanvas3D::enable_picking(bool enable) +{ + m_picking_enabled = enable; + m_selection.set_mode(Selection::Instance); +} + +void GLCanvas3D::enable_moving(bool enable) +{ + m_moving_enabled = enable; +} + +void GLCanvas3D::enable_gizmos(bool enable) +{ + m_gizmos.set_enabled(enable); +} + +void GLCanvas3D::enable_selection(bool enable) +{ + m_selection.set_enabled(enable); +} + +void GLCanvas3D::enable_main_toolbar(bool enable) +{ + m_main_toolbar.set_enabled(enable); +} + +void GLCanvas3D::enable_undoredo_toolbar(bool enable) +{ + m_undoredo_toolbar.set_enabled(enable); +} + +void GLCanvas3D::enable_dynamic_background(bool enable) +{ + m_dynamic_background_enabled = enable; +} + +void GLCanvas3D::allow_multisample(bool allow) +{ + m_multisample_allowed = allow; +} + +void GLCanvas3D::zoom_to_bed() +{ + BoundingBoxf3 box = m_bed.build_volume().bounding_volume(); + box.min.z() = 0.0; + box.max.z() = 0.0; + _zoom_to_box(box); +} + +void GLCanvas3D::zoom_to_volumes() +{ + m_apply_zoom_to_volumes_filter = true; + _zoom_to_box(volumes_bounding_box()); + m_apply_zoom_to_volumes_filter = false; +} + +void GLCanvas3D::zoom_to_selection() +{ + if (!m_selection.is_empty()) + _zoom_to_box(m_selection.get_bounding_box()); +} + +void GLCanvas3D::zoom_to_gcode() +{ + _zoom_to_box(m_gcode_viewer.get_paths_bounding_box(), 1.05); +} + +void GLCanvas3D::select_view(const std::string& direction) +{ + wxGetApp().plater()->get_camera().select_view(direction); + if (m_canvas != nullptr) + m_canvas->Refresh(); +} + +void GLCanvas3D::update_volumes_colors_by_extruder() +{ + if (m_config != nullptr) + m_volumes.update_colors_by_extruder(m_config); +} + +void GLCanvas3D::render() +{ + if (m_in_render) { + // if called recursively, return + m_dirty = true; + return; + } + + m_in_render = true; + Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; }); + (void)in_render_guard; + + if (m_canvas == nullptr) + return; + + // ensures this canvas is current and initialized + if (!_is_shown_on_screen() || !_set_current() || !wxGetApp().init_opengl()) + return; + + if (!is_initialized() && !init()) + return; + + if (!m_main_toolbar.is_enabled()) + m_gcode_viewer.init(); + + if (! m_bed.build_volume().valid()) { + // this happens at startup when no data is still saved under <>\AppData\Roaming\Slic3rPE + post_event(SimpleEvent(EVT_GLCANVAS_UPDATE_BED_SHAPE)); + return; + } + +#if ENABLE_ENVIRONMENT_MAP + if (wxGetApp().is_editor()) + wxGetApp().plater()->init_environment_texture(); +#endif // ENABLE_ENVIRONMENT_MAP + +#if ENABLE_GLMODEL_STATISTICS + GLModel::reset_statistics_counters(); +#endif // ENABLE_GLMODEL_STATISTICS + + const Size& cnv_size = get_canvas_size(); + // Probably due to different order of events on Linux/GTK2, when one switched from 3D scene + // to preview, this was called before canvas had its final size. It reported zero width + // and the viewport was set incorrectly, leading to tripping glAsserts further down + // the road (in apply_projection). That's why the minimum size is forced to 10. + Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_RAYCAST_PICKING + camera.set_viewport(0, 0, std::max(10u, (unsigned int)cnv_size.get_width()), std::max(10u, (unsigned int)cnv_size.get_height())); + camera.apply_viewport(); +#else + camera.apply_viewport(0, 0, std::max(10u, (unsigned int)cnv_size.get_width()), std::max(10u, (unsigned int)cnv_size.get_height())); +#endif // ENABLE_RAYCAST_PICKING + + if (camera.requires_zoom_to_bed) { + zoom_to_bed(); + _resize((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height()); + camera.requires_zoom_to_bed = false; + } + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + camera.apply_view_matrix(); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + camera.apply_projection(_max_bounding_box(true, true)); + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + GLfloat position_cam[4] = { 1.0f, 0.0f, 1.0f, 0.0f }; + glsafe(::glLightfv(GL_LIGHT1, GL_POSITION, position_cam)); + GLfloat position_top[4] = { -0.5f, -0.5f, 1.0f, 0.0f }; + glsafe(::glLightfv(GL_LIGHT0, GL_POSITION, position_top)); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + wxGetApp().imgui()->new_frame(); + + if (m_picking_enabled) { + if (m_rectangle_selection.is_dragging() && !m_rectangle_selection.is_empty()) + // picking pass using rectangle selection + _rectangular_selection_picking_pass(); + else if (!m_volumes.empty()) + // regular picking pass + _picking_pass(); +#if ENABLE_RAYCAST_PICKING_DEBUG + else { + ImGuiWrapper& imgui = *wxGetApp().imgui(); + imgui.begin(std::string("Hit result"), ImGuiWindowFlags_AlwaysAutoResize); + imgui.text("Picking disabled"); + imgui.end(); + } +#endif // ENABLE_RAYCAST_PICKING_DEBUG + } + + const bool is_looking_downward = camera.is_looking_downward(); + + // draw scene + glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); + _render_background(); + + _render_objects(GLVolumeCollection::ERenderType::Opaque); + if (!m_main_toolbar.is_enabled()) + _render_gcode(); + _render_sla_slices(); + _render_selection(); + if (is_looking_downward) +#if ENABLE_LEGACY_OPENGL_REMOVAL + _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), false, true); +#else + _render_bed(false, true); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + _render_objects(GLVolumeCollection::ERenderType::Transparent); + + _render_sequential_clearance(); +#if ENABLE_RENDER_SELECTION_CENTER + _render_selection_center(); +#endif // ENABLE_RENDER_SELECTION_CENTER + if (!m_main_toolbar.is_enabled()) + _render_gcode_cog(); + + // we need to set the mouse's scene position here because the depth buffer + // could be invalidated by the following gizmo render methods + // this position is used later into on_mouse() to drag the objects + if (m_picking_enabled) + m_mouse.scene_position = _mouse_to_3d(m_mouse.position.cast()); + + // sidebar hints need to be rendered before the gizmos because the depth buffer + // could be invalidated by the following gizmo render methods + _render_selection_sidebar_hints(); + _render_current_gizmo(); + if (!is_looking_downward) +#if ENABLE_LEGACY_OPENGL_REMOVAL + _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), true, true); +#else + _render_bed(true, true); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_RAYCAST_PICKING_DEBUG + if (m_picking_enabled && !m_mouse.dragging && !m_gizmos.is_dragging() && !m_rectangle_selection.is_dragging()) + m_scene_raycaster.render_hit(camera); +#endif // ENABLE_RAYCAST_PICKING_DEBUG + +#if ENABLE_SHOW_CAMERA_TARGET + _render_camera_target(); +#endif // ENABLE_SHOW_CAMERA_TARGET + + if (m_picking_enabled && m_rectangle_selection.is_dragging()) + m_rectangle_selection.render(*this); + + // draw overlays + _render_overlays(); + + if (wxGetApp().plater()->is_render_statistic_dialog_visible()) { + ImGuiWrapper& imgui = *wxGetApp().imgui(); + imgui.begin(std::string("Render statistics"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + imgui.text("FPS (SwapBuffers() calls per second):"); + ImGui::SameLine(); + imgui.text(std::to_string(m_render_stats.get_fps_and_reset_if_needed())); + ImGui::Separator(); + imgui.text("Compressed textures:"); + ImGui::SameLine(); + imgui.text(OpenGLManager::are_compressed_textures_supported() ? "supported" : "not supported"); + imgui.text("Max texture size:"); + ImGui::SameLine(); + imgui.text(std::to_string(OpenGLManager::get_gl_info().get_max_tex_size())); + imgui.end(); + } + +#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + if (wxGetApp().is_editor() && wxGetApp().plater()->is_view3D_shown()) + wxGetApp().plater()->render_project_state_debug_window(); +#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + +#if ENABLE_CAMERA_STATISTICS + camera.debug_render(); +#endif // ENABLE_CAMERA_STATISTICS +#if ENABLE_GLMODEL_STATISTICS + GLModel::render_statistics(); +#endif // ENABLE_GLMODEL_STATISTICS + + std::string tooltip; + + // Negative coordinate means out of the window, likely because the window was deactivated. + // In that case the tooltip should be hidden. + if (m_mouse.position.x() >= 0. && m_mouse.position.y() >= 0.) { + if (tooltip.empty()) + tooltip = m_layers_editing.get_tooltip(*this); + + if (tooltip.empty()) + tooltip = m_gizmos.get_tooltip(); + + if (tooltip.empty()) + tooltip = m_main_toolbar.get_tooltip(); + + if (tooltip.empty()) + tooltip = m_undoredo_toolbar.get_tooltip(); + + if (tooltip.empty()) + tooltip = wxGetApp().plater()->get_collapse_toolbar().get_tooltip(); + + if (tooltip.empty()) + tooltip = wxGetApp().plater()->get_view_toolbar().get_tooltip(); + } + + set_tooltip(tooltip); + + if (m_tooltip_enabled) + m_tooltip.render(m_mouse.position, *this); + + wxGetApp().plater()->get_mouse3d_controller().render_settings_dialog(*this); + + wxGetApp().plater()->get_notification_manager()->render_notifications(*this, get_overlay_window_width()); + + wxGetApp().imgui()->render(); + + m_canvas->SwapBuffers(); + m_render_stats.increment_fps_counter(); +} + +void GLCanvas3D::render_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, Camera::EType camera_type) +{ + render_thumbnail(thumbnail_data, w, h, thumbnail_params, m_volumes, camera_type); +} + +void GLCanvas3D::render_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type) +{ + switch (OpenGLManager::get_framebuffers_type()) + { + case OpenGLManager::EFramebufferType::Arb: { _render_thumbnail_framebuffer(thumbnail_data, w, h, thumbnail_params, volumes, camera_type); break; } + case OpenGLManager::EFramebufferType::Ext: { _render_thumbnail_framebuffer_ext(thumbnail_data, w, h, thumbnail_params, volumes, camera_type); break; } + default: { _render_thumbnail_legacy(thumbnail_data, w, h, thumbnail_params, volumes, camera_type); break; } + } +} + +void GLCanvas3D::select_all() +{ + m_selection.add_all(); + m_dirty = true; + wxGetApp().obj_manipul()->set_dirty(); + m_gizmos.reset_all_states(); + m_gizmos.update_data(); + post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); +} + +void GLCanvas3D::deselect_all() +{ + if (m_selection.is_empty()) + return; + + m_selection.remove_all(); + wxGetApp().obj_manipul()->set_dirty(); + m_gizmos.reset_all_states(); + m_gizmos.update_data(); + post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); +} + +void GLCanvas3D::delete_selected() +{ + m_selection.erase(); +} + +void GLCanvas3D::ensure_on_bed(unsigned int object_idx, bool allow_negative_z) +{ + if (allow_negative_z) + return; + + typedef std::map, double> InstancesToZMap; + InstancesToZMap instances_min_z; + + for (GLVolume* volume : m_volumes.volumes) { + if (volume->object_idx() == (int)object_idx && !volume->is_modifier) { + double min_z = volume->transformed_convex_hull_bounding_box().min.z(); + std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); + InstancesToZMap::iterator it = instances_min_z.find(instance); + if (it == instances_min_z.end()) + it = instances_min_z.insert(InstancesToZMap::value_type(instance, DBL_MAX)).first; + + it->second = std::min(it->second, min_z); + } + } + + for (GLVolume* volume : m_volumes.volumes) { + std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); + InstancesToZMap::iterator it = instances_min_z.find(instance); + if (it != instances_min_z.end()) + volume->set_instance_offset(Z, volume->get_instance_offset(Z) - it->second); + } +} + + +const std::vector& GLCanvas3D::get_gcode_layers_zs() const +{ + return m_gcode_viewer.get_layers_zs(); +} + +std::vector GLCanvas3D::get_volumes_print_zs(bool active_only) const +{ + return m_volumes.get_current_print_zs(active_only); +} + +void GLCanvas3D::set_gcode_options_visibility_from_flags(unsigned int flags) +{ + m_gcode_viewer.set_options_visibility_from_flags(flags); +} + +void GLCanvas3D::set_toolpath_role_visibility_flags(unsigned int flags) +{ + m_gcode_viewer.set_toolpath_role_visibility_flags(flags); +} + +void GLCanvas3D::set_toolpath_view_type(GCodeViewer::EViewType type) +{ + m_gcode_viewer.set_view_type(type); +} + +void GLCanvas3D::set_volumes_z_range(const std::array& range) +{ + m_volumes.set_range(range[0] - 1e-6, range[1] + 1e-6); +} + +void GLCanvas3D::set_toolpaths_z_range(const std::array& range) +{ + if (m_gcode_viewer.has_data()) + m_gcode_viewer.set_layers_z_range(range); +} + +std::vector GLCanvas3D::load_object(const ModelObject& model_object, int obj_idx, std::vector instance_idxs) +{ + if (instance_idxs.empty()) { + for (unsigned int i = 0; i < model_object.instances.size(); ++i) { + instance_idxs.emplace_back(i); + } + } +#if ENABLE_LEGACY_OPENGL_REMOVAL + return m_volumes.load_object(&model_object, obj_idx, instance_idxs); +#else + return m_volumes.load_object(&model_object, obj_idx, instance_idxs, m_initialized); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +std::vector GLCanvas3D::load_object(const Model& model, int obj_idx) +{ + if (0 <= obj_idx && obj_idx < (int)model.objects.size()) { + const ModelObject* model_object = model.objects[obj_idx]; + if (model_object != nullptr) + return load_object(*model_object, obj_idx, std::vector()); + } + + return std::vector(); +} + +void GLCanvas3D::mirror_selection(Axis axis) +{ + m_selection.mirror(axis); + do_mirror(L("Mirror Object")); + wxGetApp().obj_manipul()->set_dirty(); +} + +// Reload the 3D scene of +// 1) Model / ModelObjects / ModelInstances / ModelVolumes +// 2) Print bed +// 3) SLA support meshes for their respective ModelObjects / ModelInstances +// 4) Wipe tower preview +// 5) Out of bed collision status & message overlay (texture) +void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_refresh) +{ + if (m_canvas == nullptr || m_config == nullptr || m_model == nullptr) + return; + + if (!m_initialized) + return; + + _set_current(); + + m_hover_volume_idxs.clear(); + + struct ModelVolumeState { + ModelVolumeState(const GLVolume* volume) : + model_volume(nullptr), geometry_id(volume->geometry_id), volume_idx(-1) {} + ModelVolumeState(const ModelVolume* model_volume, const ObjectID& instance_id, const GLVolume::CompositeID& composite_id) : + model_volume(model_volume), geometry_id(std::make_pair(model_volume->id().id, instance_id.id)), composite_id(composite_id), volume_idx(-1) {} + ModelVolumeState(const ObjectID& volume_id, const ObjectID& instance_id) : + model_volume(nullptr), geometry_id(std::make_pair(volume_id.id, instance_id.id)), volume_idx(-1) {} + bool new_geometry() const { return this->volume_idx == size_t(-1); } + const ModelVolume* model_volume; + // ObjectID of ModelVolume + ObjectID of ModelInstance + // or timestamp of an SLAPrintObjectStep + ObjectID of ModelInstance + std::pair geometry_id; + GLVolume::CompositeID composite_id; + // Volume index in the new GLVolume vector. + size_t volume_idx; + }; + std::vector model_volume_state; + std::vector aux_volume_state; + + struct GLVolumeState { + GLVolumeState() : + volume_idx(size_t(-1)) {} + GLVolumeState(const GLVolume* volume, unsigned int volume_idx) : + composite_id(volume->composite_id), volume_idx(volume_idx) {} + GLVolumeState(const GLVolume::CompositeID &composite_id) : + composite_id(composite_id), volume_idx(size_t(-1)) {} + + GLVolume::CompositeID composite_id; + // Volume index in the old GLVolume vector. + size_t volume_idx; + }; + + // SLA steps to pull the preview meshes for. + typedef std::array SLASteps; + SLASteps sla_steps = { slaposDrillHoles, slaposSupportTree, slaposPad }; + struct SLASupportState { + std::array::value> step; + }; + // State of the sla_steps for all SLAPrintObjects. + std::vector sla_support_state; + + std::vector instance_ids_selected; + std::vector map_glvolume_old_to_new(m_volumes.volumes.size(), size_t(-1)); + std::vector deleted_volumes; + std::vector glvolumes_new; + glvolumes_new.reserve(m_volumes.volumes.size()); + auto model_volume_state_lower = [](const ModelVolumeState& m1, const ModelVolumeState& m2) { return m1.geometry_id < m2.geometry_id; }; + + m_reload_delayed = !m_canvas->IsShown() && !refresh_immediately && !force_full_scene_refresh; + + PrinterTechnology printer_technology = current_printer_technology(); + int volume_idx_wipe_tower_old = -1; + + // Release invalidated volumes to conserve GPU memory in case of delayed refresh (see m_reload_delayed). + // First initialize model_volumes_new_sorted & model_instances_new_sorted. + for (int object_idx = 0; object_idx < (int)m_model->objects.size(); ++object_idx) { + const ModelObject* model_object = m_model->objects[object_idx]; + for (int instance_idx = 0; instance_idx < (int)model_object->instances.size(); ++instance_idx) { + const ModelInstance* model_instance = model_object->instances[instance_idx]; + for (int volume_idx = 0; volume_idx < (int)model_object->volumes.size(); ++volume_idx) { + const ModelVolume* model_volume = model_object->volumes[volume_idx]; + model_volume_state.emplace_back(model_volume, model_instance->id(), GLVolume::CompositeID(object_idx, volume_idx, instance_idx)); + } + } + } + if (printer_technology == ptSLA) { + const SLAPrint* sla_print = this->sla_print(); +#ifndef NDEBUG + // Verify that the SLAPrint object is synchronized with m_model. + check_model_ids_equal(*m_model, sla_print->model()); +#endif /* NDEBUG */ + sla_support_state.reserve(sla_print->objects().size()); + for (const SLAPrintObject* print_object : sla_print->objects()) { + SLASupportState state; + for (size_t istep = 0; istep < sla_steps.size(); ++istep) { + state.step[istep] = print_object->step_state_with_timestamp(sla_steps[istep]); + if (state.step[istep].state == PrintStateBase::DONE) { + if (!print_object->has_mesh(sla_steps[istep])) + // Consider the DONE step without a valid mesh as invalid for the purpose + // of mesh visualization. + state.step[istep].state = PrintStateBase::INVALID; + else if (sla_steps[istep] != slaposDrillHoles) + for (const ModelInstance* model_instance : print_object->model_object()->instances) + // Only the instances, which are currently printable, will have the SLA support structures kept. + // The instances outside the print bed will have the GLVolumes of their support structures released. + if (model_instance->is_printable()) + aux_volume_state.emplace_back(state.step[istep].timestamp, model_instance->id()); + } + } + sla_support_state.emplace_back(state); + } + } + std::sort(model_volume_state.begin(), model_volume_state.end(), model_volume_state_lower); + std::sort(aux_volume_state.begin(), aux_volume_state.end(), model_volume_state_lower); + // Release all ModelVolume based GLVolumes not found in the current Model. Find the GLVolume of a hollowed mesh. + for (size_t volume_id = 0; volume_id < m_volumes.volumes.size(); ++volume_id) { + GLVolume* volume = m_volumes.volumes[volume_id]; + ModelVolumeState key(volume); + ModelVolumeState* mvs = nullptr; + if (volume->volume_idx() < 0) { + auto it = std::lower_bound(aux_volume_state.begin(), aux_volume_state.end(), key, model_volume_state_lower); + if (it != aux_volume_state.end() && it->geometry_id == key.geometry_id) + // This can be an SLA support structure that should not be rendered (in case someone used undo + // to revert to before it was generated). We only reuse the volume if that's not the case. + if (m_model->objects[volume->composite_id.object_id]->sla_points_status != sla::PointsStatus::NoPoints) + mvs = &(*it); + } + else { + auto it = std::lower_bound(model_volume_state.begin(), model_volume_state.end(), key, model_volume_state_lower); + if (it != model_volume_state.end() && it->geometry_id == key.geometry_id) + mvs = &(*it); + } + // Emplace instance ID of the volume. Both the aux volumes and model volumes share the same instance ID. + // The wipe tower has its own wipe_tower_instance_id(). + if (m_selection.contains_volume(volume_id)) + instance_ids_selected.emplace_back(volume->geometry_id.second); + if (mvs == nullptr || force_full_scene_refresh) { + // This GLVolume will be released. + if (volume->is_wipe_tower) { + // There is only one wipe tower. + assert(volume_idx_wipe_tower_old == -1); +#if ENABLE_OPENGL_ES + m_wipe_tower_mesh.clear(); +#endif // ENABLE_OPENGL_ES + volume_idx_wipe_tower_old = (int)volume_id; + } + if (!m_reload_delayed) { + deleted_volumes.emplace_back(volume, volume_id); + delete volume; + } + } + else { + // This GLVolume will be reused. + volume->set_sla_shift_z(0.0); + map_glvolume_old_to_new[volume_id] = glvolumes_new.size(); + mvs->volume_idx = glvolumes_new.size(); + glvolumes_new.emplace_back(volume); + // Update color of the volume based on the current extruder. + if (mvs->model_volume != nullptr) { + int extruder_id = mvs->model_volume->extruder_id(); + if (extruder_id != -1) + volume->extruder_id = extruder_id; + + volume->is_modifier = !mvs->model_volume->is_model_part(); + volume->set_color(color_from_model_volume(*mvs->model_volume)); + // force update of render_color alpha channel + volume->set_render_color(volume->color.is_transparent()); + + // updates volumes transformations + volume->set_instance_transformation(mvs->model_volume->get_object()->instances[mvs->composite_id.instance_id]->get_transformation()); + volume->set_volume_transformation(mvs->model_volume->get_transformation()); + + // updates volumes convex hull + if (mvs->model_volume->is_model_part() && ! volume->convex_hull()) + // Model volume was likely changed from modifier or support blocker / enforcer to a model part. + // Only model parts require convex hulls. + volume->set_convex_hull(mvs->model_volume->get_convex_hull_shared_ptr()); + } + } + } + sort_remove_duplicates(instance_ids_selected); + auto deleted_volumes_lower = [](const GLVolumeState &v1, const GLVolumeState &v2) { return v1.composite_id < v2.composite_id; }; + std::sort(deleted_volumes.begin(), deleted_volumes.end(), deleted_volumes_lower); + + if (m_reload_delayed) + return; + + bool update_object_list = false; + if (m_volumes.volumes != glvolumes_new) + update_object_list = true; + m_volumes.volumes = std::move(glvolumes_new); + for (unsigned int obj_idx = 0; obj_idx < (unsigned int)m_model->objects.size(); ++ obj_idx) { + const ModelObject &model_object = *m_model->objects[obj_idx]; + for (int volume_idx = 0; volume_idx < (int)model_object.volumes.size(); ++ volume_idx) { + const ModelVolume &model_volume = *model_object.volumes[volume_idx]; + for (int instance_idx = 0; instance_idx < (int)model_object.instances.size(); ++ instance_idx) { + const ModelInstance &model_instance = *model_object.instances[instance_idx]; + ModelVolumeState key(model_volume.id(), model_instance.id()); + auto it = std::lower_bound(model_volume_state.begin(), model_volume_state.end(), key, model_volume_state_lower); + assert(it != model_volume_state.end() && it->geometry_id == key.geometry_id); + if (it->new_geometry()) { + // New volume. + auto it_old_volume = std::lower_bound(deleted_volumes.begin(), deleted_volumes.end(), GLVolumeState(it->composite_id), deleted_volumes_lower); + if (it_old_volume != deleted_volumes.end() && it_old_volume->composite_id == it->composite_id) + // If a volume changed its ObjectID, but it reuses a GLVolume's CompositeID, maintain its selection. + map_glvolume_old_to_new[it_old_volume->volume_idx] = m_volumes.volumes.size(); + // Note the index of the loaded volume, so that we can reload the main model GLVolume with the hollowed mesh + // later in this function. + it->volume_idx = m_volumes.volumes.size(); +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_volumes.load_object_volume(&model_object, obj_idx, volume_idx, instance_idx); +#else + m_volumes.load_object_volume(&model_object, obj_idx, volume_idx, instance_idx, m_initialized); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + m_volumes.volumes.back()->geometry_id = key.geometry_id; + update_object_list = true; + } else { + // Recycling an old GLVolume. + GLVolume &existing_volume = *m_volumes.volumes[it->volume_idx]; + assert(existing_volume.geometry_id == key.geometry_id); + // Update the Object/Volume/Instance indices into the current Model. + if (existing_volume.composite_id != it->composite_id) { + existing_volume.composite_id = it->composite_id; + update_object_list = true; + } + } + } + } + } + if (printer_technology == ptSLA) { + size_t idx = 0; + const SLAPrint *sla_print = this->sla_print(); + std::vector shift_zs(m_model->objects.size(), 0); + double relative_correction_z = sla_print->relative_correction().z(); + if (relative_correction_z <= EPSILON) + relative_correction_z = 1.; + for (const SLAPrintObject *print_object : sla_print->objects()) { + SLASupportState &state = sla_support_state[idx ++]; + const ModelObject *model_object = print_object->model_object(); + // Find an index of the ModelObject + int object_idx; + // There may be new SLA volumes added to the scene for this print_object. + // Find the object index of this print_object in the Model::objects list. + auto it = std::find(sla_print->model().objects.begin(), sla_print->model().objects.end(), model_object); + assert(it != sla_print->model().objects.end()); + object_idx = it - sla_print->model().objects.begin(); + // Cache the Z offset to be applied to all volumes with this object_idx. + shift_zs[object_idx] = print_object->get_current_elevation() / relative_correction_z; + // Collect indices of this print_object's instances, for which the SLA support meshes are to be added to the scene. + // pairs of + std::vector> instances[std::tuple_size::value]; + for (size_t print_instance_idx = 0; print_instance_idx < print_object->instances().size(); ++ print_instance_idx) { + const SLAPrintObject::Instance &instance = print_object->instances()[print_instance_idx]; + // Find index of ModelInstance corresponding to this SLAPrintObject::Instance. + auto it = std::find_if(model_object->instances.begin(), model_object->instances.end(), + [&instance](const ModelInstance *mi) { return mi->id() == instance.instance_id; }); + assert(it != model_object->instances.end()); + int instance_idx = it - model_object->instances.begin(); + for (size_t istep = 0; istep < sla_steps.size(); ++ istep) + if (sla_steps[istep] == slaposDrillHoles) { + // Hollowing is a special case, where the mesh from the backend is being loaded into the 1st volume of an instance, + // not into its own GLVolume. + // There shall always be such a GLVolume allocated. + ModelVolumeState key(model_object->volumes.front()->id(), instance.instance_id); + auto it = std::lower_bound(model_volume_state.begin(), model_volume_state.end(), key, model_volume_state_lower); + assert(it != model_volume_state.end() && it->geometry_id == key.geometry_id); + assert(!it->new_geometry()); + GLVolume &volume = *m_volumes.volumes[it->volume_idx]; + if (! volume.offsets.empty() && state.step[istep].timestamp != volume.offsets.front()) { + // The backend either produced a new hollowed mesh, or it invalidated the one that the front end has seen. +#if ENABLE_LEGACY_OPENGL_REMOVAL + volume.model.reset(); +#else + volume.indexed_vertex_array.release_geometry(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + if (state.step[istep].state == PrintStateBase::DONE) { + TriangleMesh mesh = print_object->get_mesh(slaposDrillHoles); + assert(! mesh.empty()); + + // sla_trafo does not contain volume trafo. To get a mesh to create + // a new volume from, we have to apply vol trafo inverse separately. + const ModelObject& mo = *m_model->objects[volume.object_idx()]; + Transform3d trafo = sla_print->sla_trafo(mo) + * mo.volumes.front()->get_transformation().get_matrix(); + mesh.transform(trafo.inverse()); +#if ENABLE_SMOOTH_NORMALS +#if ENABLE_LEGACY_OPENGL_REMOVAL + volume.model.init_from(mesh, true); +#else + volume.indexed_vertex_array.load_mesh(mesh, true); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#else +#if ENABLE_LEGACY_OPENGL_REMOVAL + volume.model.init_from(mesh); +#if ENABLE_RAYCAST_PICKING + volume.mesh_raycaster = std::make_unique(std::make_shared(mesh)); +#endif // ENABLE_RAYCAST_PICKING +#else + volume.indexed_vertex_array.load_mesh(mesh); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#endif // ENABLE_SMOOTH_NORMALS + } + else { + // Reload the original volume. +#if ENABLE_SMOOTH_NORMALS +#if ENABLE_LEGACY_OPENGL_REMOVAL + volume.model.init_from(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(), true); +#else + volume.indexed_vertex_array.load_mesh(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(), true); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#else +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_RAYCAST_PICKING + const TriangleMesh& new_mesh = m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(); + volume.model.init_from(new_mesh); + volume.mesh_raycaster = std::make_unique(std::make_shared(new_mesh)); +#else + volume.model.init_from(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh()); +#endif // ENABLE_RAYCAST_PICKING +#else + volume.indexed_vertex_array.load_mesh(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#endif // ENABLE_SMOOTH_NORMALS + } +#if !ENABLE_LEGACY_OPENGL_REMOVAL + volume.finalize_geometry(true); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + } + //FIXME it is an ugly hack to write the timestamp into the "offsets" field to not have to add another member variable + // to the GLVolume. We should refactor GLVolume significantly, so that the GLVolume will not contain member variables + // of various concenrs (model vs. 3D print path). + volume.offsets = { state.step[istep].timestamp }; + } + else if (state.step[istep].state == PrintStateBase::DONE) { + // Check whether there is an existing auxiliary volume to be updated, or a new auxiliary volume to be created. + ModelVolumeState key(state.step[istep].timestamp, instance.instance_id.id); + auto it = std::lower_bound(aux_volume_state.begin(), aux_volume_state.end(), key, model_volume_state_lower); + assert(it != aux_volume_state.end() && it->geometry_id == key.geometry_id); + if (it->new_geometry()) { + // This can be an SLA support structure that should not be rendered (in case someone used undo + // to revert to before it was generated). If that's the case, we should not generate anything. + if (model_object->sla_points_status != sla::PointsStatus::NoPoints) + instances[istep].emplace_back(std::pair(instance_idx, print_instance_idx)); + else + shift_zs[object_idx] = 0.; + } + else { + // Recycling an old GLVolume. Update the Object/Instance indices into the current Model. + m_volumes.volumes[it->volume_idx]->composite_id = GLVolume::CompositeID(object_idx, m_volumes.volumes[it->volume_idx]->volume_idx(), instance_idx); + m_volumes.volumes[it->volume_idx]->set_instance_transformation(model_object->instances[instance_idx]->get_transformation()); + } + } + } + + for (size_t istep = 0; istep < sla_steps.size(); ++istep) + if (!instances[istep].empty()) +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_volumes.load_object_auxiliary(print_object, object_idx, instances[istep], sla_steps[istep], state.step[istep].timestamp); +#else + m_volumes.load_object_auxiliary(print_object, object_idx, instances[istep], sla_steps[istep], state.step[istep].timestamp, m_initialized); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + + // Shift-up all volumes of the object so that it has the right elevation with respect to the print bed + for (GLVolume* volume : m_volumes.volumes) + if (volume->object_idx() < (int)m_model->objects.size() && m_model->objects[volume->object_idx()]->instances[volume->instance_idx()]->is_printable()) + volume->set_sla_shift_z(shift_zs[volume->object_idx()]); + } + + if (printer_technology == ptFFF && m_config->has("nozzle_diameter")) { + // Should the wipe tower be visualized ? + unsigned int extruders_count = (unsigned int)dynamic_cast(m_config->option("nozzle_diameter"))->values.size(); + + bool wt = dynamic_cast(m_config->option("wipe_tower"))->value; + bool co = dynamic_cast(m_config->option("complete_objects"))->value; + + if (extruders_count > 1 && wt && !co) { + // Height of a print (Show at least a slab) + double height = std::max(m_model->bounding_box().max(2), 10.0); + + float x = dynamic_cast(m_config->option("wipe_tower_x"))->value; + float y = dynamic_cast(m_config->option("wipe_tower_y"))->value; + float w = dynamic_cast(m_config->option("wipe_tower_width"))->value; + float a = dynamic_cast(m_config->option("wipe_tower_rotation_angle"))->value; + + const Print *print = m_process->fff_print(); + + float depth = print->wipe_tower_data(extruders_count).depth; + float brim_width = print->wipe_tower_data(extruders_count).brim_width; + +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_OPENGL_ES + int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview( + x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower), + brim_width, &m_wipe_tower_mesh); +#else + int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview( + x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower), + brim_width); +#endif // ENABLE_OPENGL_ES +#else + int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview( + x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower), + brim_width, m_initialized); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + if (volume_idx_wipe_tower_old != -1) + map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new; + } + } + + update_volumes_colors_by_extruder(); + // Update selection indices based on the old/new GLVolumeCollection. + if (m_selection.get_mode() == Selection::Instance) + m_selection.instances_changed(instance_ids_selected); + else + m_selection.volumes_changed(map_glvolume_old_to_new); + + m_gizmos.update_data(); + m_gizmos.refresh_on_off_state(); + + // Update the toolbar + if (update_object_list) + post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); + + // checks for geometry outside the print volume to render it accordingly + if (!m_volumes.empty()) { + ModelInstanceEPrintVolumeState state; + const bool contained_min_one = m_volumes.check_outside_state(m_bed.build_volume(), &state); + const bool partlyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Partly_Outside); + const bool fullyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Fully_Outside); + + if (printer_technology != ptSLA) { + _set_warning_notification(EWarning::ObjectClashed, partlyOut); + _set_warning_notification(EWarning::ObjectOutside, fullyOut); + _set_warning_notification(EWarning::SlaSupportsOutside, false); + } + else { + const auto [res, volume] = _is_any_volume_outside(); + const bool is_support = volume != nullptr && volume->is_sla_support(); + if (is_support) { + _set_warning_notification(EWarning::ObjectClashed, false); + _set_warning_notification(EWarning::ObjectOutside, false); + _set_warning_notification(EWarning::SlaSupportsOutside, partlyOut || fullyOut); + } + else { + _set_warning_notification(EWarning::ObjectClashed, partlyOut); + _set_warning_notification(EWarning::ObjectOutside, fullyOut); + _set_warning_notification(EWarning::SlaSupportsOutside, false); + } + } + + post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, + contained_min_one && !m_model->objects.empty() && !partlyOut)); + } + else { + _set_warning_notification(EWarning::ObjectOutside, false); + _set_warning_notification(EWarning::ObjectClashed, false); + _set_warning_notification(EWarning::SlaSupportsOutside, false); + post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); + } + + refresh_camera_scene_box(); + + if (m_selection.is_empty()) { + // If no object is selected, deactivate the active gizmo, if any + // Otherwise it may be shown after cleaning the scene (if it was active while the objects were deleted) + m_gizmos.reset_all_states(); + + // If no object is selected, reset the objects manipulator on the sidebar + // to force a reset of its cache + auto manip = wxGetApp().obj_manipul(); + if (manip != nullptr) + manip->set_dirty(); + } + +#if ENABLE_RAYCAST_PICKING + // refresh volume raycasters for picking + m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::Volume); + for (size_t i = 0; i < m_volumes.volumes.size(); ++i) { + assert(m_volumes.volumes[i]->mesh_raycaster != nullptr); + add_raycaster_for_picking(SceneRaycaster::EType::Volume, i, *m_volumes.volumes[i]->mesh_raycaster, m_volumes.volumes[i]->world_matrix()); + } + + // refresh gizmo elements raycasters for picking + GLGizmoBase* curr_gizmo = m_gizmos.get_current(); + if (curr_gizmo != nullptr) + curr_gizmo->unregister_raycasters_for_picking(); + m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::Gizmo); + if (curr_gizmo != nullptr && !m_selection.is_empty()) + curr_gizmo->register_raycasters_for_picking(); +#endif // ENABLE_RAYCAST_PICKING + + // and force this canvas to be redrawn. + m_dirty = true; +} + +#if !ENABLE_LEGACY_OPENGL_REMOVAL +static void reserve_new_volume_finalize_old_volume(GLVolume& vol_new, GLVolume& vol_old, bool gl_initialized, size_t prealloc_size = VERTEX_BUFFER_RESERVE_SIZE) +{ + // Assign the large pre-allocated buffers to the new GLVolume. + vol_new.indexed_vertex_array = std::move(vol_old.indexed_vertex_array); + // Copy the content back to the old GLVolume. + vol_old.indexed_vertex_array = vol_new.indexed_vertex_array; + // Clear the buffers, but keep them pre-allocated. + vol_new.indexed_vertex_array.clear(); + // Just make sure that clear did not clear the reserved memory. + // Reserving number of vertices (3x position + 3x color) + vol_new.indexed_vertex_array.reserve(prealloc_size / 6); + // Finalize the old geometry, possibly move data to the graphics card. + vol_old.finalize_geometry(gl_initialized); +} +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + +void GLCanvas3D::load_gcode_preview(const GCodeProcessorResult& gcode_result, const std::vector& str_tool_colors) +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_gcode_viewer.load(gcode_result, *this->fff_print()); +#else + m_gcode_viewer.load(gcode_result, *this->fff_print(), m_initialized); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + if (wxGetApp().is_editor()) { + m_gcode_viewer.update_shells_color_by_extruder(m_config); + _set_warning_notification_if_needed(EWarning::ToolpathOutside); + } + + m_gcode_viewer.refresh(gcode_result, str_tool_colors); + set_as_dirty(); + request_extra_frame(); +} + +void GLCanvas3D::refresh_gcode_preview_render_paths(bool keep_sequential_current_first, bool keep_sequential_current_last) +{ + m_gcode_viewer.refresh_render_paths(keep_sequential_current_first, keep_sequential_current_last); + set_as_dirty(); + request_extra_frame(); +} + +void GLCanvas3D::load_sla_preview() +{ + const SLAPrint* print = sla_print(); + if (m_canvas != nullptr && print != nullptr) { + _set_current(); + // Release OpenGL data before generating new data. + reset_volumes(); + _load_sla_shells(); + _update_sla_shells_outside_state(); + _set_warning_notification_if_needed(EWarning::ObjectClashed); + _set_warning_notification_if_needed(EWarning::SlaSupportsOutside); + } +} + +void GLCanvas3D::load_preview(const std::vector& str_tool_colors, const std::vector& color_print_values) +{ + const Print *print = this->fff_print(); + if (print == nullptr) + return; + + _set_current(); + + // Release OpenGL data before generating new data. + this->reset_volumes(); + + const BuildVolume &build_volume = m_bed.build_volume(); + _load_print_toolpaths(build_volume); + _load_wipe_tower_toolpaths(build_volume, str_tool_colors); + for (const PrintObject* object : print->objects()) + _load_print_object_toolpaths(*object, build_volume, str_tool_colors, color_print_values); + + _set_warning_notification_if_needed(EWarning::ToolpathOutside); +} + +void GLCanvas3D::bind_event_handlers() +{ + if (m_canvas != nullptr) { + m_canvas->Bind(wxEVT_SIZE, &GLCanvas3D::on_size, this); + m_canvas->Bind(wxEVT_IDLE, &GLCanvas3D::on_idle, this); + m_canvas->Bind(wxEVT_CHAR, &GLCanvas3D::on_char, this); + m_canvas->Bind(wxEVT_KEY_DOWN, &GLCanvas3D::on_key, this); + m_canvas->Bind(wxEVT_KEY_UP, &GLCanvas3D::on_key, this); + m_canvas->Bind(wxEVT_MOUSEWHEEL, &GLCanvas3D::on_mouse_wheel, this); + m_canvas->Bind(wxEVT_TIMER, &GLCanvas3D::on_timer, this); + m_canvas->Bind(EVT_GLCANVAS_RENDER_TIMER, &GLCanvas3D::on_render_timer, this); + m_toolbar_highlighter.set_timer_owner(m_canvas, 0); + m_canvas->Bind(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_toolbar_highlighter.blink(); }); + m_gizmo_highlighter.set_timer_owner(m_canvas, 0); + m_canvas->Bind(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_gizmo_highlighter.blink(); }); + m_canvas->Bind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_MIDDLE_UP, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_RIGHT_DOWN, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_RIGHT_UP, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_MOTION, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_ENTER_WINDOW, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_LEAVE_WINDOW, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_LEFT_DCLICK, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_PAINT, &GLCanvas3D::on_paint, this); + m_canvas->Bind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this); + + m_event_handlers_bound = true; + } +} + +void GLCanvas3D::unbind_event_handlers() +{ + if (m_canvas != nullptr && m_event_handlers_bound) { + m_canvas->Unbind(wxEVT_SIZE, &GLCanvas3D::on_size, this); + m_canvas->Unbind(wxEVT_IDLE, &GLCanvas3D::on_idle, this); + m_canvas->Unbind(wxEVT_CHAR, &GLCanvas3D::on_char, this); + m_canvas->Unbind(wxEVT_KEY_DOWN, &GLCanvas3D::on_key, this); + m_canvas->Unbind(wxEVT_KEY_UP, &GLCanvas3D::on_key, this); + m_canvas->Unbind(wxEVT_MOUSEWHEEL, &GLCanvas3D::on_mouse_wheel, this); + m_canvas->Unbind(wxEVT_TIMER, &GLCanvas3D::on_timer, this); + m_canvas->Unbind(EVT_GLCANVAS_RENDER_TIMER, &GLCanvas3D::on_render_timer, this); + m_canvas->Unbind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_MIDDLE_UP, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_RIGHT_DOWN, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_RIGHT_UP, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_MOTION, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_ENTER_WINDOW, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_LEAVE_WINDOW, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_LEFT_DCLICK, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_PAINT, &GLCanvas3D::on_paint, this); + m_canvas->Unbind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this); + + m_event_handlers_bound = false; + } +} + +void GLCanvas3D::on_size(wxSizeEvent& evt) +{ + m_dirty = true; +} + +void GLCanvas3D::on_idle(wxIdleEvent& evt) +{ + if (!m_initialized) + return; + + m_dirty |= m_main_toolbar.update_items_state(); + m_dirty |= m_undoredo_toolbar.update_items_state(); + m_dirty |= wxGetApp().plater()->get_view_toolbar().update_items_state(); + m_dirty |= wxGetApp().plater()->get_collapse_toolbar().update_items_state(); + bool mouse3d_controller_applied = wxGetApp().plater()->get_mouse3d_controller().apply(wxGetApp().plater()->get_camera()); + m_dirty |= mouse3d_controller_applied; + m_dirty |= wxGetApp().plater()->get_notification_manager()->update_notifications(*this); + auto gizmo = wxGetApp().plater()->canvas3D()->get_gizmos_manager().get_current(); + if (gizmo != nullptr) m_dirty |= gizmo->update_items_state(); + // ImGuiWrapper::m_requires_extra_frame may have been set by a render made outside of the OnIdle mechanism + bool imgui_requires_extra_frame = wxGetApp().imgui()->requires_extra_frame(); + m_dirty |= imgui_requires_extra_frame; + + if (!m_dirty) + return; + + // this needs to be done here. + // during the render launched by the refresh the value may be set again + wxGetApp().imgui()->reset_requires_extra_frame(); + + _refresh_if_shown_on_screen(); + + if (m_extra_frame_requested || mouse3d_controller_applied || imgui_requires_extra_frame || wxGetApp().imgui()->requires_extra_frame()) { + m_extra_frame_requested = false; + evt.RequestMore(); + } + else + m_dirty = false; +} + +void GLCanvas3D::on_char(wxKeyEvent& evt) +{ + if (!m_initialized) + return; + + // see include/wx/defs.h enum wxKeyCode + int keyCode = evt.GetKeyCode(); + int ctrlMask = wxMOD_CONTROL; + int shiftMask = wxMOD_SHIFT; + + auto imgui = wxGetApp().imgui(); + if (imgui->update_key_data(evt)) { + render(); + return; + } + + if (keyCode == WXK_ESCAPE && (_deactivate_undo_redo_toolbar_items() || _deactivate_search_toolbar_item() || _deactivate_arrange_menu())) + return; + + if (m_gizmos.on_char(evt)) + return; + + if ((evt.GetModifiers() & ctrlMask) != 0) { + // CTRL is pressed + switch (keyCode) { +#ifdef __APPLE__ + case 'a': + case 'A': +#else /* __APPLE__ */ + case WXK_CONTROL_A: +#endif /* __APPLE__ */ + post_event(SimpleEvent(EVT_GLCANVAS_SELECT_ALL)); + break; +#ifdef __APPLE__ + case 'c': + case 'C': +#else /* __APPLE__ */ + case WXK_CONTROL_C: +#endif /* __APPLE__ */ + post_event(SimpleEvent(EVT_GLTOOLBAR_COPY)); + break; +#ifdef __APPLE__ + case 'm': + case 'M': +#else /* __APPLE__ */ + case WXK_CONTROL_M: +#endif /* __APPLE__ */ + { +#ifdef _WIN32 + if (wxGetApp().app_config->get("use_legacy_3DConnexion") == "1") { +#endif //_WIN32 +#ifdef __APPLE__ + // On OSX use Cmd+Shift+M to "Show/Hide 3Dconnexion devices settings dialog" + if ((evt.GetModifiers() & shiftMask) != 0) { +#endif // __APPLE__ + Mouse3DController& controller = wxGetApp().plater()->get_mouse3d_controller(); + controller.show_settings_dialog(!controller.is_settings_dialog_shown()); + m_dirty = true; +#ifdef __APPLE__ + } + else + // and Cmd+M to minimize application + wxGetApp().mainframe->Iconize(); +#endif // __APPLE__ +#ifdef _WIN32 + } +#endif //_WIN32 + break; + } +#ifdef __APPLE__ + case 'v': + case 'V': +#else /* __APPLE__ */ + case WXK_CONTROL_V: +#endif /* __APPLE__ */ + post_event(SimpleEvent(EVT_GLTOOLBAR_PASTE)); + break; + + +#ifdef __APPLE__ + case 'f': + case 'F': +#else /* __APPLE__ */ + case WXK_CONTROL_F: +#endif /* __APPLE__ */ + _activate_search_toolbar_item(); + break; + + +#ifdef __APPLE__ + case 'y': + case 'Y': +#else /* __APPLE__ */ + case WXK_CONTROL_Y: +#endif /* __APPLE__ */ + post_event(SimpleEvent(EVT_GLCANVAS_REDO)); + break; +#ifdef __APPLE__ + case 'z': + case 'Z': +#else /* __APPLE__ */ + case WXK_CONTROL_Z: +#endif /* __APPLE__ */ + post_event(SimpleEvent(EVT_GLCANVAS_UNDO)); + break; + + case WXK_BACK: + case WXK_DELETE: + post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE_ALL)); break; + default: evt.Skip(); + } + } else { + switch (keyCode) + { + case WXK_BACK: + case WXK_DELETE: { post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE)); break; } + case WXK_ESCAPE: { deselect_all(); break; } + case WXK_F5: { + if ((wxGetApp().is_editor() && !wxGetApp().plater()->model().objects.empty()) || + (wxGetApp().is_gcode_viewer() && !wxGetApp().plater()->get_last_loaded_gcode().empty())) + post_event(SimpleEvent(EVT_GLCANVAS_RELOAD_FROM_DISK)); + break; + } + case '0': { select_view("iso"); break; } + case '1': { select_view("top"); break; } + case '2': { select_view("bottom"); break; } + case '3': { select_view("front"); break; } + case '4': { select_view("rear"); break; } + case '5': { select_view("left"); break; } + case '6': { select_view("right"); break; } + case '+': { + if (dynamic_cast(m_canvas->GetParent()) != nullptr) + post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt)); + else + post_event(Event(EVT_GLCANVAS_INCREASE_INSTANCES, +1)); + break; + } + case '-': { + if (dynamic_cast(m_canvas->GetParent()) != nullptr) + post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt)); + else + post_event(Event(EVT_GLCANVAS_INCREASE_INSTANCES, -1)); + break; + } + case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; } + case 'A': + case 'a': { post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE)); break; } + case 'B': + case 'b': { zoom_to_bed(); break; } + case 'C': + case 'c': { m_gcode_viewer.toggle_gcode_window_visibility(); m_dirty = true; request_extra_frame(); break; } + case 'E': + case 'e': { m_labels.show(!m_labels.is_shown()); m_dirty = true; break; } + case 'G': + case 'g': { + if ((evt.GetModifiers() & shiftMask) != 0) { + if (dynamic_cast(m_canvas->GetParent()) != nullptr) + post_event(wxKeyEvent(EVT_GLCANVAS_JUMP_TO, evt)); + } + break; + } + case 'I': + case 'i': { _update_camera_zoom(1.0); break; } + case 'K': + case 'k': { wxGetApp().plater()->get_camera().select_next_type(); m_dirty = true; break; } + case 'L': + case 'l': { + if (!m_main_toolbar.is_enabled()) + show_legend(!is_legend_shown()); + break; + } + case 'O': + case 'o': { _update_camera_zoom(-1.0); break; } + case 'Z': + case 'z': { + if (!m_selection.is_empty()) + zoom_to_selection(); + else { + if (!m_volumes.empty()) + zoom_to_volumes(); + else + _zoom_to_box(m_gcode_viewer.get_paths_bounding_box()); + } + break; + } + default: { evt.Skip(); break; } + } + } +} + +class TranslationProcessor +{ + using UpAction = std::function; + using DownAction = std::function; + + UpAction m_up_action{ nullptr }; + DownAction m_down_action{ nullptr }; + + bool m_running{ false }; + Vec3d m_direction{ Vec3d::UnitX() }; + +public: + TranslationProcessor(UpAction up_action, DownAction down_action) + : m_up_action(up_action), m_down_action(down_action) + { + } + + void process(wxKeyEvent& evt) + { + const int keyCode = evt.GetKeyCode(); + wxEventType type = evt.GetEventType(); + if (type == wxEVT_KEY_UP) { + switch (keyCode) + { + case WXK_NUMPAD_LEFT: case WXK_LEFT: + case WXK_NUMPAD_RIGHT: case WXK_RIGHT: + case WXK_NUMPAD_UP: case WXK_UP: + case WXK_NUMPAD_DOWN: case WXK_DOWN: + { + m_running = false; + m_up_action(); + break; + } + default: { break; } + } + } + else if (type == wxEVT_KEY_DOWN) { + bool apply = false; + + switch (keyCode) + { + case WXK_SHIFT: + { + if (m_running) + apply = true; + + break; + } + case WXK_NUMPAD_LEFT: + case WXK_LEFT: + { + m_direction = -Vec3d::UnitX(); + apply = true; + break; + } + case WXK_NUMPAD_RIGHT: + case WXK_RIGHT: + { + m_direction = Vec3d::UnitX(); + apply = true; + break; + } + case WXK_NUMPAD_UP: + case WXK_UP: + { + m_direction = Vec3d::UnitY(); + apply = true; + break; + } + case WXK_NUMPAD_DOWN: + case WXK_DOWN: + { + m_direction = -Vec3d::UnitY(); + apply = true; + break; + } + default: { break; } + } + + if (apply) { + m_running = true; + m_down_action(m_direction, evt.ShiftDown(), evt.CmdDown()); + } + } + } +}; + +void GLCanvas3D::on_key(wxKeyEvent& evt) +{ + static TranslationProcessor translationProcessor( + [this]() { + do_move(L("Gizmo-Move")); + m_gizmos.update_data(); + + wxGetApp().obj_manipul()->set_dirty(); + // Let the plater know that the dragging finished, so a delayed refresh + // of the scene with the background processing data should be performed. + post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); + // updates camera target constraints + refresh_camera_scene_box(); + m_dirty = true; + }, + [this](const Vec3d& direction, bool slow, bool camera_space) { + m_selection.setup_cache(); + double multiplier = slow ? 1.0 : 10.0; + + Vec3d displacement; + if (camera_space) { + Eigen::Matrix inv_view_3x3 = wxGetApp().plater()->get_camera().get_view_matrix().inverse().matrix().block(0, 0, 3, 3); + displacement = multiplier * (inv_view_3x3 * direction); + displacement.z() = 0.0; + } + else + displacement = multiplier * direction; + +#if ENABLE_WORLD_COORDINATE + TransformationType trafo_type; + trafo_type.set_relative(); + m_selection.translate(displacement, trafo_type); +#else + m_selection.translate(displacement); +#endif // ENABLE_WORLD_COORDINATE + m_dirty = true; + } + ); + + const int keyCode = evt.GetKeyCode(); + + auto imgui = wxGetApp().imgui(); + if (imgui->update_key_data(evt)) { + render(); + } + else { + if (!m_gizmos.on_key(evt)) { + if (evt.GetEventType() == wxEVT_KEY_UP) { + if (evt.ShiftDown() && evt.ControlDown() && keyCode == WXK_SPACE) { + wxGetApp().plater()->toggle_render_statistic_dialog(); + m_dirty = true; + } + if (m_tab_down && keyCode == WXK_TAB && !evt.HasAnyModifiers()) { + // Enable switching between 3D and Preview with Tab + // m_canvas->HandleAsNavigationKey(evt); // XXX: Doesn't work in some cases / on Linux + post_event(SimpleEvent(EVT_GLCANVAS_TAB)); + } + else if (keyCode == WXK_TAB && evt.ShiftDown() && ! wxGetApp().is_gcode_viewer()) { + // Collapse side-panel with Shift+Tab + post_event(SimpleEvent(EVT_GLCANVAS_COLLAPSE_SIDEBAR)); + } + else if (keyCode == WXK_SHIFT) { + translationProcessor.process(evt); + + if (m_picking_enabled && m_rectangle_selection.is_dragging()) { + _update_selection_from_hover(); + m_rectangle_selection.stop_dragging(); + m_mouse.ignore_left_up = true; + } + m_shift_kar_filter.reset_count(); + m_dirty = true; +// set_cursor(Standard); + } + else if (keyCode == WXK_ALT) { + if (m_picking_enabled && m_rectangle_selection.is_dragging()) { + _update_selection_from_hover(); + m_rectangle_selection.stop_dragging(); + m_mouse.ignore_left_up = true; + m_dirty = true; + } +// set_cursor(Standard); + } + else if (keyCode == WXK_CONTROL) { +#if ENABLE_NEW_CAMERA_MOVEMENTS +#if ENABLE_RAYCAST_PICKING + if (m_mouse.dragging && !m_moving) { +#else + if (m_mouse.dragging) { +#endif // ENABLE_RAYCAST_PICKING + // if the user releases CTRL while rotating the 3D scene + // prevent from moving the selected volume + m_mouse.drag.move_volume_idx = -1; + m_mouse.set_start_position_3D_as_invalid(); + } +#endif // ENABLE_NEW_CAMERA_MOVEMENTS + m_ctrl_kar_filter.reset_count(); + m_dirty = true; + } + else if (m_gizmos.is_enabled() && !m_selection.is_empty()) { + translationProcessor.process(evt); + + switch (keyCode) + { + case WXK_NUMPAD_PAGEUP: case WXK_PAGEUP: + case WXK_NUMPAD_PAGEDOWN: case WXK_PAGEDOWN: + { + do_rotate(L("Gizmo-Rotate")); + m_gizmos.update_data(); + + wxGetApp().obj_manipul()->set_dirty(); + // Let the plater know that the dragging finished, so a delayed refresh + // of the scene with the background processing data should be performed. + post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); + // updates camera target constraints + refresh_camera_scene_box(); + m_dirty = true; + + break; + } + default: { break; } + } + } + } + else if (evt.GetEventType() == wxEVT_KEY_DOWN) { + m_tab_down = keyCode == WXK_TAB && !evt.HasAnyModifiers(); + if (keyCode == WXK_SHIFT) { + translationProcessor.process(evt); + + if (m_picking_enabled && (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports)) { + m_mouse.ignore_left_up = false; +// set_cursor(Cross); + } + if (m_shift_kar_filter.is_first()) + m_dirty = true; + + m_shift_kar_filter.increase_count(); + } + else if (keyCode == WXK_ALT) { + if (m_picking_enabled && (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports)) { + m_mouse.ignore_left_up = false; +// set_cursor(Cross); + } + } + else if (keyCode == WXK_CONTROL) { + if (m_ctrl_kar_filter.is_first()) + m_dirty = true; + + m_ctrl_kar_filter.increase_count(); + } + else if (m_gizmos.is_enabled() && !m_selection.is_empty()) { + auto do_rotate = [this](double angle_z_rad) { + m_selection.setup_cache(); + m_selection.rotate(Vec3d(0.0, 0.0, angle_z_rad), TransformationType(TransformationType::World_Relative_Joint)); + m_dirty = true; +// wxGetApp().obj_manipul()->set_dirty(); + }; + + translationProcessor.process(evt); + + switch (keyCode) + { + case WXK_NUMPAD_PAGEUP: case WXK_PAGEUP: { do_rotate(0.25 * M_PI); break; } + case WXK_NUMPAD_PAGEDOWN: case WXK_PAGEDOWN: { do_rotate(-0.25 * M_PI); break; } + default: { break; } + } + } + else if (!m_gizmos.is_enabled()) { + // DoubleSlider navigation in Preview + if (keyCode == WXK_LEFT || + keyCode == WXK_RIGHT || + keyCode == WXK_UP || + keyCode == WXK_DOWN) { + if (dynamic_cast(m_canvas->GetParent()) != nullptr) + post_event(wxKeyEvent(EVT_GLCANVAS_MOVE_SLIDERS, evt)); + } + } + } + } + } + + if (keyCode != WXK_TAB + && keyCode != WXK_LEFT + && keyCode != WXK_UP + && keyCode != WXK_RIGHT + && keyCode != WXK_DOWN) { + evt.Skip(); // Needed to have EVT_CHAR generated as well + } +} + +void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt) +{ +#ifdef WIN32 + // Try to filter out spurious mouse wheel events comming from 3D mouse. + if (wxGetApp().plater()->get_mouse3d_controller().process_mouse_wheel()) + return; +#endif + + if (!m_initialized) + return; + + // Ignore the wheel events if the middle button is pressed. + if (evt.MiddleIsDown()) + return; + +#if ENABLE_RETINA_GL + const float scale = m_retina_helper->get_scale_factor(); + evt.SetX(evt.GetX() * scale); + evt.SetY(evt.GetY() * scale); +#endif + + if (wxGetApp().imgui()->update_mouse_data(evt)) { + m_dirty = true; + return; + } + +#ifdef __WXMSW__ + // For some reason the Idle event is not being generated after the mouse scroll event in case of scrolling with the two fingers on the touch pad, + // if the event is not allowed to be passed further. + // https://github.com/prusa3d/PrusaSlicer/issues/2750 + // evt.Skip() used to trigger the needed screen refresh, but it does no more. wxWakeUpIdle() seem to work now. + wxWakeUpIdle(); +#endif /* __WXMSW__ */ + + // Performs layers editing updates, if enabled + if (is_layers_editing_enabled()) { + int object_idx_selected = m_selection.get_object_idx(); + if (object_idx_selected != -1) { + // A volume is selected. Test, whether hovering over a layer thickness bar. + if (m_layers_editing.bar_rect_contains(*this, (float)evt.GetX(), (float)evt.GetY())) { + // Adjust the width of the selection. + m_layers_editing.band_width = std::max(std::min(m_layers_editing.band_width * (1.0f + 0.1f * (float)evt.GetWheelRotation() / (float)evt.GetWheelDelta()), 10.0f), 1.5f); + if (m_canvas != nullptr) + m_canvas->Refresh(); + + return; + } + } + } + + // If the Search window or Undo/Redo list is opened, + // update them according to the event + if (m_main_toolbar.is_item_pressed("search") || + m_undoredo_toolbar.is_item_pressed("undo") || + m_undoredo_toolbar.is_item_pressed("redo")) { + m_mouse_wheel = int((double)evt.GetWheelRotation() / (double)evt.GetWheelDelta()); + return; + } + + // Inform gizmos about the event so they have the opportunity to react. + if (m_gizmos.on_mouse_wheel(evt)) + return; + + // Calculate the zoom delta and apply it to the current zoom factor + double direction_factor = (wxGetApp().app_config->get("reverse_mouse_wheel_zoom") == "1") ? -1.0 : 1.0; + _update_camera_zoom(direction_factor * (double)evt.GetWheelRotation() / (double)evt.GetWheelDelta()); +} + +void GLCanvas3D::on_timer(wxTimerEvent& evt) +{ + if (m_layers_editing.state == LayersEditing::Editing) + _perform_layer_editing_action(); +} + +void GLCanvas3D::on_render_timer(wxTimerEvent& evt) +{ + // no need to wake up idle + // right after this event, idle event is fired + // m_dirty = true; + // wxWakeUpIdle(); +} + + +void GLCanvas3D::schedule_extra_frame(int miliseconds) +{ + // Schedule idle event right now + if (miliseconds == 0) + { + // We want to wakeup idle evnt but most likely this is call inside render cycle so we need to wait + if (m_in_render) + miliseconds = 33; + else { + m_dirty = true; + wxWakeUpIdle(); + return; + } + } + int remaining_time = m_render_timer.GetInterval(); + // Timer is not running + if (!m_render_timer.IsRunning()) { + m_render_timer.StartOnce(miliseconds); + // Timer is running - restart only if new period is shorter than remaning period + } else { + if (miliseconds + 20 < remaining_time) { + m_render_timer.Stop(); + m_render_timer.StartOnce(miliseconds); + } + } +} + +#ifndef NDEBUG +// #define SLIC3R_DEBUG_MOUSE_EVENTS +#endif + +#ifdef SLIC3R_DEBUG_MOUSE_EVENTS +std::string format_mouse_event_debug_message(const wxMouseEvent &evt) +{ + static int idx = 0; + char buf[2048]; + std::string out; + sprintf(buf, "Mouse Event %d - ", idx ++); + out = buf; + + if (evt.Entering()) + out += "Entering "; + if (evt.Leaving()) + out += "Leaving "; + if (evt.Dragging()) + out += "Dragging "; + if (evt.Moving()) + out += "Moving "; + if (evt.Magnify()) + out += "Magnify "; + if (evt.LeftDown()) + out += "LeftDown "; + if (evt.LeftUp()) + out += "LeftUp "; + if (evt.LeftDClick()) + out += "LeftDClick "; + if (evt.MiddleDown()) + out += "MiddleDown "; + if (evt.MiddleUp()) + out += "MiddleUp "; + if (evt.MiddleDClick()) + out += "MiddleDClick "; + if (evt.RightDown()) + out += "RightDown "; + if (evt.RightUp()) + out += "RightUp "; + if (evt.RightDClick()) + out += "RightDClick "; + + sprintf(buf, "(%d, %d)", evt.GetX(), evt.GetY()); + out += buf; + return out; +} +#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */ + +void GLCanvas3D::on_mouse(wxMouseEvent& evt) +{ + if (!m_initialized || !_set_current()) + return; + +#if ENABLE_RETINA_GL + const float scale = m_retina_helper->get_scale_factor(); + evt.SetX(evt.GetX() * scale); + evt.SetY(evt.GetY() * scale); +#endif + + Point pos(evt.GetX(), evt.GetY()); + + ImGuiWrapper* imgui = wxGetApp().imgui(); + if (m_tooltip.is_in_imgui() && evt.LeftUp()) + // ignore left up events coming from imgui windows and not processed by them + m_mouse.ignore_left_up = true; + m_tooltip.set_in_imgui(false); + if (imgui->update_mouse_data(evt)) { + m_mouse.position = evt.Leaving() ? Vec2d(-1.0, -1.0) : pos.cast(); + m_tooltip.set_in_imgui(true); + render(); +#ifdef SLIC3R_DEBUG_MOUSE_EVENTS + printf((format_mouse_event_debug_message(evt) + " - Consumed by ImGUI\n").c_str()); +#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */ + m_dirty = true; + // do not return if dragging or tooltip not empty to allow for tooltip update + // also, do not return if the mouse is moving and also is inside MM gizmo to allow update seed fill selection + if (!m_mouse.dragging && m_tooltip.is_empty() && (m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation || !evt.Moving())) + return; + } + +#ifdef __WXMSW__ + bool on_enter_workaround = false; + if (! evt.Entering() && ! evt.Leaving() && m_mouse.position.x() == -1.0) { + // Workaround for SPE-832: There seems to be a mouse event sent to the window before evt.Entering() + m_mouse.position = pos.cast(); + render(); +#ifdef SLIC3R_DEBUG_MOUSE_EVENTS + printf((format_mouse_event_debug_message(evt) + " - OnEnter workaround\n").c_str()); +#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */ + on_enter_workaround = true; + } else +#endif /* __WXMSW__ */ + { +#ifdef SLIC3R_DEBUG_MOUSE_EVENTS + printf((format_mouse_event_debug_message(evt) + " - other\n").c_str()); +#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */ + } + + if (m_main_toolbar.on_mouse(evt, *this)) { + if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) + mouse_up_cleanup(); + m_mouse.set_start_position_3D_as_invalid(); + return; + } + + if (m_undoredo_toolbar.on_mouse(evt, *this)) { + if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) + mouse_up_cleanup(); + m_mouse.set_start_position_3D_as_invalid(); + return; + } + + if (wxGetApp().plater()->get_collapse_toolbar().on_mouse(evt, *this)) { + if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) + mouse_up_cleanup(); + m_mouse.set_start_position_3D_as_invalid(); + return; + } + + if (wxGetApp().plater()->get_view_toolbar().on_mouse(evt, *this)) { + if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) + mouse_up_cleanup(); + m_mouse.set_start_position_3D_as_invalid(); + return; + } + + for (GLVolume* volume : m_volumes.volumes) { + volume->force_sinking_contours = false; + } + + auto show_sinking_contours = [this]() { + const Selection::IndicesList& idxs = m_selection.get_volume_idxs(); + for (unsigned int idx : idxs) { + m_volumes.volumes[idx]->force_sinking_contours = true; + } + m_dirty = true; + }; + + if (m_gizmos.on_mouse(evt)) { + if (wxWindow::FindFocus() != m_canvas) + // Grab keyboard focus for input in gizmo dialogs. + m_canvas->SetFocus(); + + if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) + mouse_up_cleanup(); + + m_mouse.set_start_position_3D_as_invalid(); + m_mouse.position = pos.cast(); + + // It should be detection of volume change + // Not only detection of some modifiers !!! + if (evt.Dragging()) { + GLGizmosManager::EType c = m_gizmos.get_current_type(); + if (current_printer_technology() == ptFFF && + fff_print()->config().complete_objects){ + if (c == GLGizmosManager::EType::Move || + c == GLGizmosManager::EType::Scale || + c == GLGizmosManager::EType::Rotate ) + update_sequential_clearance(); + } else { + if (c == GLGizmosManager::EType::Move || + c == GLGizmosManager::EType::Scale || + c == GLGizmosManager::EType::Rotate) + show_sinking_contours(); + } + } + else if (evt.LeftUp() && + m_gizmos.get_current_type() == GLGizmosManager::EType::Scale && + m_gizmos.get_current()->get_state() == GLGizmoBase::EState::On) { + wxGetApp().obj_list()->selection_changed(); + } + + return; + } + + bool any_gizmo_active = m_gizmos.get_current() != nullptr; + + int selected_object_idx = m_selection.get_object_idx(); + int layer_editing_object_idx = is_layers_editing_enabled() ? selected_object_idx : -1; + m_layers_editing.select_object(*m_model, layer_editing_object_idx); + + if (m_mouse.drag.move_requires_threshold && m_mouse.is_move_start_threshold_position_2D_defined() && m_mouse.is_move_threshold_met(pos)) { + m_mouse.drag.move_requires_threshold = false; + m_mouse.set_move_start_threshold_position_2D_as_invalid(); + } + + if (evt.ButtonDown() && wxWindow::FindFocus() != m_canvas) + // Grab keyboard focus on any mouse click event. + m_canvas->SetFocus(); + + if (evt.Entering()) { +//#if defined(__WXMSW__) || defined(__linux__) +// // On Windows and Linux needs focus in order to catch key events + // Set focus in order to remove it from object list + if (m_canvas != nullptr) { + // Only set focus, if the top level window of this canvas is active + // and ObjectList has a focus + auto p = dynamic_cast(evt.GetEventObject()); + while (p->GetParent()) + p = p->GetParent(); +#ifdef __WIN32__ + wxWindow* const obj_list = wxGetApp().obj_list()->GetMainWindow(); +#else + wxWindow* const obj_list = wxGetApp().obj_list(); +#endif + if (obj_list == p->FindFocus()) + m_canvas->SetFocus(); + m_mouse.position = pos.cast(); + m_tooltip_enabled = false; + // 1) forces a frame render to ensure that m_hover_volume_idxs is updated even when the user right clicks while + // the context menu is shown, ensuring it to disappear if the mouse is outside any volume and to + // change the volume hover state if any is under the mouse + // 2) when switching between 3d view and preview the size of the canvas changes if the side panels are visible, + // so forces a resize to avoid multiple renders with different sizes (seen as flickering) + _refresh_if_shown_on_screen(); + m_tooltip_enabled = true; + } + m_mouse.set_start_position_2D_as_invalid(); +//#endif + } + else if (evt.Leaving()) { + _deactivate_undo_redo_toolbar_items(); + + // to remove hover on objects when the mouse goes out of this canvas + m_mouse.position = Vec2d(-1.0, -1.0); + m_dirty = true; + } + else if (evt.LeftDown() || evt.RightDown() || evt.MiddleDown()) { + if (_deactivate_undo_redo_toolbar_items() || _deactivate_search_toolbar_item() || _deactivate_arrange_menu()) + return; + + // If user pressed left or right button we first check whether this happened + // on a volume or not. + m_layers_editing.state = LayersEditing::Unknown; + if (layer_editing_object_idx != -1 && m_layers_editing.bar_rect_contains(*this, pos(0), pos(1))) { + // A volume is selected and the mouse is inside the layer thickness bar. + // Start editing the layer height. + m_layers_editing.state = LayersEditing::Editing; + _perform_layer_editing_action(&evt); + } + else { + const bool rectangle_selection_dragging = m_rectangle_selection.is_dragging(); + if (evt.LeftDown() && (evt.ShiftDown() || evt.AltDown()) && m_picking_enabled) { + if (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports && + m_gizmos.get_current_type() != GLGizmosManager::FdmSupports && + m_gizmos.get_current_type() != GLGizmosManager::Seam && + m_gizmos.get_current_type() != GLGizmosManager::Cut && + m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation) { + m_rectangle_selection.start_dragging(m_mouse.position, evt.ShiftDown() ? GLSelectionRectangle::EState::Select : GLSelectionRectangle::EState::Deselect); + m_dirty = true; + } + } + + // Select volume in this 3D canvas. + // Don't deselect a volume if layer editing is enabled or any gizmo is active. We want the object to stay selected + // during the scene manipulation. + + if (m_picking_enabled && (!any_gizmo_active || !evt.CmdDown()) && (!m_hover_volume_idxs.empty() || !is_layers_editing_enabled()) && !rectangle_selection_dragging) { + if (evt.LeftDown() && !m_hover_volume_idxs.empty()) { + int volume_idx = get_first_hover_volume_idx(); + bool already_selected = m_selection.contains_volume(volume_idx); + bool shift_down = evt.ShiftDown(); + + Selection::IndicesList curr_idxs = m_selection.get_volume_idxs(); + + if (already_selected && shift_down) + m_selection.remove(volume_idx); + else { + m_selection.add(volume_idx, !shift_down, true); + m_mouse.drag.move_requires_threshold = !already_selected; + if (already_selected) + m_mouse.set_move_start_threshold_position_2D_as_invalid(); + else + m_mouse.drag.move_start_threshold_position_2D = pos; + } + + // propagate event through callback + if (curr_idxs != m_selection.get_volume_idxs()) { + if (m_selection.is_empty()) + m_gizmos.reset_all_states(); + else + m_gizmos.refresh_on_off_state(); + + m_gizmos.update_data(); + post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); + m_dirty = true; + } + } + } + + if (!m_hover_volume_idxs.empty() && !m_rectangle_selection.is_dragging()) { + if (evt.LeftDown() && m_moving_enabled && m_mouse.drag.move_volume_idx == -1) { + // Only accept the initial position, if it is inside the volume bounding box. + const int volume_idx = get_first_hover_volume_idx(); + BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box(); + volume_bbox.offset(1.0); + const bool is_cut_connector_selected = m_selection.is_any_connector(); + if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(m_mouse.scene_position) && !is_cut_connector_selected) { + m_volumes.volumes[volume_idx]->hover = GLVolume::HS_None; + // The dragging operation is initiated. + m_mouse.drag.move_volume_idx = volume_idx; +#if ENABLE_NEW_CAMERA_MOVEMENTS + m_selection.setup_cache(); + if (!evt.CmdDown()) +#endif // ENABLE_NEW_CAMERA_MOVEMENTS + m_mouse.drag.start_position_3D = m_mouse.scene_position; + m_sequential_print_clearance_first_displacement = true; +#if !ENABLE_RAYCAST_PICKING + m_moving = true; +#endif // !ENABLE_RAYCAST_PICKING + } + } + } + } + } +#if ENABLE_NEW_CAMERA_MOVEMENTS + else if (evt.Dragging() && evt.LeftIsDown() && !evt.CmdDown() && m_layers_editing.state == LayersEditing::Unknown && + m_mouse.drag.move_volume_idx != -1 && m_mouse.is_start_position_3D_defined()) { +#else + else if (evt.Dragging() && evt.LeftIsDown() && m_layers_editing.state == LayersEditing::Unknown && m_mouse.drag.move_volume_idx != -1) { +#endif // ENABLE_NEW_CAMERA_MOVEMENTS + if (!m_mouse.drag.move_requires_threshold) { + m_mouse.dragging = true; + Vec3d cur_pos = m_mouse.drag.start_position_3D; + // we do not want to translate objects if the user just clicked on an object while pressing shift to remove it from the selection and then drag + if (m_selection.contains_volume(get_first_hover_volume_idx())) { + const Camera& camera = wxGetApp().plater()->get_camera(); + if (std::abs(camera.get_dir_forward().z()) < EPSILON) { + // side view -> move selected volumes orthogonally to camera view direction + const Linef3 ray = mouse_ray(pos); + const Vec3d dir = ray.unit_vector(); + // finds the intersection of the mouse ray with the plane parallel to the camera viewport and passing throught the starting position + // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebric form + // in our case plane normal and ray direction are the same (orthogonal view) + // when moving to perspective camera the negative z unit axis of the camera needs to be transformed in world space and used as plane normal + const Vec3d inters = ray.a + (m_mouse.drag.start_position_3D - ray.a).dot(dir) / dir.squaredNorm() * dir; + // vector from the starting position to the found intersection + const Vec3d inters_vec = inters - m_mouse.drag.start_position_3D; + + const Vec3d camera_right = camera.get_dir_right(); + const Vec3d camera_up = camera.get_dir_up(); + + // finds projection of the vector along the camera axes + const double projection_x = inters_vec.dot(camera_right); + const double projection_z = inters_vec.dot(camera_up); + + // apply offset + cur_pos = m_mouse.drag.start_position_3D + projection_x * camera_right + projection_z * camera_up; + } + else { + // Generic view + // Get new position at the same Z of the initial click point. + cur_pos = mouse_ray(pos).intersect_plane(m_mouse.drag.start_position_3D.z()); + } + } + +#if ENABLE_WORLD_COORDINATE +#if ENABLE_RAYCAST_PICKING + m_moving = true; +#endif // ENABLE_RAYCAST_PICKING + TransformationType trafo_type; + trafo_type.set_relative(); + m_selection.translate(cur_pos - m_mouse.drag.start_position_3D, trafo_type); +#else + m_selection.translate(cur_pos - m_mouse.drag.start_position_3D); +#endif // ENABLE_WORLD_COORDINATE + if (current_printer_technology() == ptFFF && fff_print()->config().complete_objects) + update_sequential_clearance(); + wxGetApp().obj_manipul()->set_dirty(); + m_dirty = true; + } + } + else if (evt.Dragging() && evt.LeftIsDown() && m_picking_enabled && m_rectangle_selection.is_dragging()) { + // keeps the mouse position updated while dragging the selection rectangle + m_mouse.position = pos.cast(); + m_rectangle_selection.dragging(m_mouse.position); + m_dirty = true; + } + else if (evt.Dragging()) { + m_mouse.dragging = true; + + if (m_layers_editing.state != LayersEditing::Unknown && layer_editing_object_idx != -1) { + if (m_layers_editing.state == LayersEditing::Editing) { + _perform_layer_editing_action(&evt); + m_mouse.position = pos.cast(); + } + } + // do not process the dragging if the left mouse was set down in another canvas +#if ENABLE_NEW_CAMERA_MOVEMENTS + else if (evt.LeftIsDown()) { + // if dragging over blank area with left button, rotate +#if ENABLE_RAYCAST_PICKING + if (!m_moving) { +#endif // ENABLE_RAYCAST_PICKING + if ((any_gizmo_active || evt.CmdDown() || m_hover_volume_idxs.empty()) && m_mouse.is_start_position_3D_defined()) { +#else + // if dragging over blank area with left button, rotate + else if (evt.LeftIsDown()) { + if ((any_gizmo_active || m_hover_volume_idxs.empty()) && m_mouse.is_start_position_3D_defined()) { +#endif // ENABLE_NEW_CAMERA_MOVEMENTS + const Vec3d rot = (Vec3d(pos.x(), pos.y(), 0.0) - m_mouse.drag.start_position_3D) * (PI * TRACKBALLSIZE / 180.0); + if (wxGetApp().app_config->get("use_free_camera") == "1") + // Virtual track ball (similar to the 3DConnexion mouse). + wxGetApp().plater()->get_camera().rotate_local_around_target(Vec3d(rot.y(), rot.x(), 0.0)); + else { + // Forces camera right vector to be parallel to XY plane in case it has been misaligned using the 3D mouse free rotation. + // It is cheaper to call this function right away instead of testing wxGetApp().plater()->get_mouse3d_controller().connected(), + // which checks an atomics (flushes CPU caches). + // See GH issue #3816. + Camera& camera = wxGetApp().plater()->get_camera(); + camera.recover_from_free_camera(); + camera.rotate_on_sphere(rot.x(), rot.y(), current_printer_technology() != ptSLA); + } + + m_dirty = true; + } + m_mouse.drag.start_position_3D = Vec3d((double)pos.x(), (double)pos.y(), 0.0); +#if ENABLE_RAYCAST_PICKING + } +#endif // ENABLE_RAYCAST_PICKING + } + else if (evt.MiddleIsDown() || evt.RightIsDown()) { + // If dragging over blank area with right/middle button, pan. + if (m_mouse.is_start_position_2D_defined()) { + // get point in model space at Z = 0 + float z = 0.0f; + const Vec3d cur_pos = _mouse_to_3d(pos, &z); + const Vec3d orig = _mouse_to_3d(m_mouse.drag.start_position_2D, &z); + Camera& camera = wxGetApp().plater()->get_camera(); + if (wxGetApp().app_config->get("use_free_camera") != "1") + // Forces camera right vector to be parallel to XY plane in case it has been misaligned using the 3D mouse free rotation. + // It is cheaper to call this function right away instead of testing wxGetApp().plater()->get_mouse3d_controller().connected(), + // which checks an atomics (flushes CPU caches). + // See GH issue #3816. + camera.recover_from_free_camera(); + + camera.set_target(camera.get_target() + orig - cur_pos); + m_dirty = true; + } + + m_mouse.drag.start_position_2D = pos; + } + } + else if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) { +#if ENABLE_RAYCAST_PICKING + m_mouse.position = pos.cast(); +#endif // ENABLE_RAYCAST_PICKING + + if (m_layers_editing.state != LayersEditing::Unknown) { + m_layers_editing.state = LayersEditing::Unknown; + _stop_timer(); + m_layers_editing.accept_changes(*this); + } + else if (m_mouse.drag.move_volume_idx != -1 && m_mouse.dragging) { + do_move(L("Move Object")); + wxGetApp().obj_manipul()->set_dirty(); + // Let the plater know that the dragging finished, so a delayed refresh + // of the scene with the background processing data should be performed. + post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); + } + else if (evt.LeftUp() && m_picking_enabled && m_rectangle_selection.is_dragging()) { + if (evt.ShiftDown() || evt.AltDown()) + _update_selection_from_hover(); + + m_rectangle_selection.stop_dragging(); + } + else if (evt.LeftUp() && !m_mouse.ignore_left_up && !m_mouse.dragging && m_hover_volume_idxs.empty() && !is_layers_editing_enabled()) { + // deselect and propagate event through callback + if (!evt.ShiftDown() && (!any_gizmo_active || !evt.CmdDown()) && m_picking_enabled) + deselect_all(); + } + else if (evt.RightUp()) { +#if !ENABLE_RAYCAST_PICKING + m_mouse.position = pos.cast(); +#endif // !ENABLE_RAYCAST_PICKING + // forces a frame render to ensure that m_hover_volume_idxs is updated even when the user right clicks while + // the context menu is already shown + render(); + if (!m_hover_volume_idxs.empty()) { + // if right clicking on volume, propagate event through callback (shows context menu) + int volume_idx = get_first_hover_volume_idx(); + if (!m_volumes.volumes[volume_idx]->is_wipe_tower // no context menu for the wipe tower + && m_gizmos.get_current_type() != GLGizmosManager::SlaSupports) // disable context menu when the gizmo is open + { + // forces the selection of the volume + /* m_selection.add(volume_idx); // #et_FIXME_if_needed + * To avoid extra "Add-Selection" snapshots, + * call add() with check_for_already_contained=true + * */ + m_selection.add(volume_idx, true, true); + m_gizmos.refresh_on_off_state(); + post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); + m_gizmos.update_data(); + wxGetApp().obj_manipul()->set_dirty(); + // forces a frame render to update the view before the context menu is shown + render(); + } + } + Vec2d logical_pos = pos.cast(); +#if ENABLE_RETINA_GL + const float factor = m_retina_helper->get_scale_factor(); + logical_pos = logical_pos.cwiseQuotient(Vec2d(factor, factor)); +#endif // ENABLE_RETINA_GL + if (!m_mouse.dragging) { + // do not post the event if the user is panning the scene + // or if right click was done over the wipe tower + const bool post_right_click_event = m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower; + if (post_right_click_event) + post_event(RBtnEvent(EVT_GLCANVAS_RIGHT_CLICK, { logical_pos, m_hover_volume_idxs.empty() })); + } + } + + mouse_up_cleanup(); + } + else if (evt.Moving()) { + m_mouse.position = pos.cast(); + + // updates gizmos overlay + if (m_selection.is_empty()) + m_gizmos.reset_all_states(); + + m_dirty = true; + } + else + evt.Skip(); + + if (m_moving) + show_sinking_contours(); + +#ifdef __WXMSW__ + if (on_enter_workaround) + m_mouse.position = Vec2d(-1., -1.); +#endif /* __WXMSW__ */ +} + +void GLCanvas3D::on_paint(wxPaintEvent& evt) +{ + if (m_initialized) + m_dirty = true; + else + // Call render directly, so it gets initialized immediately, not from On Idle handler. + this->render(); +} + +void GLCanvas3D::on_set_focus(wxFocusEvent& evt) +{ + m_tooltip_enabled = false; + _refresh_if_shown_on_screen(); + m_tooltip_enabled = true; +} + +Size GLCanvas3D::get_canvas_size() const +{ + int w = 0; + int h = 0; + + if (m_canvas != nullptr) + m_canvas->GetSize(&w, &h); + +#if ENABLE_RETINA_GL + const float factor = m_retina_helper->get_scale_factor(); + w *= factor; + h *= factor; +#else + const float factor = 1.0f; +#endif + + return Size(w, h, factor); +} + +Vec2d GLCanvas3D::get_local_mouse_position() const +{ + if (m_canvas == nullptr) + return Vec2d::Zero(); + + wxPoint mouse_pos = m_canvas->ScreenToClient(wxGetMousePosition()); + const double factor = +#if ENABLE_RETINA_GL + m_retina_helper->get_scale_factor(); +#else + 1.0; +#endif + return Vec2d(factor * mouse_pos.x, factor * mouse_pos.y); +} + +void GLCanvas3D::set_tooltip(const std::string& tooltip) +{ + if (m_canvas != nullptr) + m_tooltip.set_text(tooltip); +} + +void GLCanvas3D::do_move(const std::string& snapshot_type) +{ + if (m_model == nullptr) + return; + + if (!snapshot_type.empty()) + wxGetApp().plater()->take_snapshot(_(snapshot_type)); + + std::set> done; // keeps track of modified instances + bool object_moved = false; + Vec3d wipe_tower_origin = Vec3d::Zero(); + + Selection::EMode selection_mode = m_selection.get_mode(); + + for (const GLVolume* v : m_volumes.volumes) { + int object_idx = v->object_idx(); + int instance_idx = v->instance_idx(); + int volume_idx = v->volume_idx(); + + std::pair done_id(object_idx, instance_idx); + + if (0 <= object_idx && object_idx < (int)m_model->objects.size()) { + done.insert(done_id); + + // Move instances/volumes + ModelObject* model_object = m_model->objects[object_idx]; + if (model_object != nullptr) { + if (selection_mode == Selection::Instance) +#if ENABLE_WORLD_COORDINATE + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); +#else + model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); +#endif // ENABLE_WORLD_COORDINATE + else if (selection_mode == Selection::Volume) +#if ENABLE_WORLD_COORDINATE + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); +#else + model_object->volumes[volume_idx]->set_offset(v->get_volume_offset()); +#endif // ENABLE_WORLD_COORDINATE + + object_moved = true; + model_object->invalidate_bounding_box(); + } + } + else if (v->is_wipe_tower) + // Move a wipe tower proxy. + wipe_tower_origin = v->get_volume_offset(); + } + + // Fixes flying instances + for (const std::pair& i : done) { + ModelObject* m = m_model->objects[i.first]; + const double shift_z = m->get_instance_min_z(i.second); + if (current_printer_technology() == ptSLA || shift_z > SINKING_Z_THRESHOLD) { + const Vec3d shift(0.0, 0.0, -shift_z); + m_selection.translate(i.first, i.second, shift); + m->translate_instance(i.second, shift); + } + wxGetApp().obj_list()->update_info_items(static_cast(i.first)); + } + + // if the selection is not valid to allow for layer editing after the move, we need to turn off the tool if it is running + // similar to void Plater::priv::selection_changed() + if (!wxGetApp().plater()->can_layers_editing() && is_layers_editing_enabled()) + post_event(SimpleEvent(EVT_GLTOOLBAR_LAYERSEDITING)); + + if (object_moved) + post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED)); + + if (wipe_tower_origin != Vec3d::Zero()) + post_event(Vec3dEvent(EVT_GLCANVAS_WIPETOWER_MOVED, std::move(wipe_tower_origin))); + + reset_sequential_print_clearance(); + + m_dirty = true; +} + +void GLCanvas3D::do_rotate(const std::string& snapshot_type) +{ + if (m_model == nullptr) + return; + + if (!snapshot_type.empty()) + wxGetApp().plater()->take_snapshot(_(snapshot_type)); + + // stores current min_z of instances + std::map, double> min_zs; + for (int i = 0; i < static_cast(m_model->objects.size()); ++i) { + const ModelObject* obj = m_model->objects[i]; + for (int j = 0; j < static_cast(obj->instances.size()); ++j) { + if (snapshot_type == L("Gizmo-Place on Face") && m_selection.get_object_idx() == i) { + // This means we are flattening this object. In that case pretend + // that it is not sinking (even if it is), so it is placed on bed + // later on (whatever is sinking will be left sinking). + min_zs[{ i, j }] = SINKING_Z_THRESHOLD; + } + else + min_zs[{ i, j }] = obj->instance_bounding_box(j).min.z(); + + } + } + + std::set> done; // keeps track of modified instances + + Selection::EMode selection_mode = m_selection.get_mode(); + + for (const GLVolume* v : m_volumes.volumes) { + if (v->is_wipe_tower) { + const Vec3d offset = v->get_volume_offset(); + post_event(Vec3dEvent(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3d(offset.x(), offset.y(), v->get_volume_rotation().z()))); + } + const int object_idx = v->object_idx(); + if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) + continue; + + const int instance_idx = v->instance_idx(); + const int volume_idx = v->volume_idx(); + + done.insert(std::pair(object_idx, instance_idx)); + + // Rotate instances/volumes. + ModelObject* model_object = m_model->objects[object_idx]; + if (model_object != nullptr) { + if (selection_mode == Selection::Instance) { +#if ENABLE_WORLD_COORDINATE + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); +#else + model_object->instances[instance_idx]->set_rotation(v->get_instance_rotation()); + model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); +#endif // ENABLE_WORLD_COORDINATE + } + else if (selection_mode == Selection::Volume) { +#if ENABLE_WORLD_COORDINATE + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); +#else + model_object->volumes[volume_idx]->set_rotation(v->get_volume_rotation()); + model_object->volumes[volume_idx]->set_offset(v->get_volume_offset()); +#endif // ENABLE_WORLD_COORDINATE + } + model_object->invalidate_bounding_box(); + } + } + + // Fixes sinking/flying instances + for (const std::pair& i : done) { + ModelObject* m = m_model->objects[i.first]; + const double shift_z = m->get_instance_min_z(i.second); + // leave sinking instances as sinking + if (min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) { + const Vec3d shift(0.0, 0.0, -shift_z); + m_selection.translate(i.first, i.second, shift); + m->translate_instance(i.second, shift); + } + + wxGetApp().obj_list()->update_info_items(static_cast(i.first)); + } + + if (!done.empty()) + post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED)); + + m_dirty = true; +} + +void GLCanvas3D::do_scale(const std::string& snapshot_type) +{ + if (m_model == nullptr) + return; + + if (!snapshot_type.empty()) + wxGetApp().plater()->take_snapshot(_(snapshot_type)); + + // stores current min_z of instances + std::map, double> min_zs; + if (!snapshot_type.empty()) { + for (int i = 0; i < static_cast(m_model->objects.size()); ++i) { + const ModelObject* obj = m_model->objects[i]; + for (int j = 0; j < static_cast(obj->instances.size()); ++j) { + min_zs[{ i, j }] = obj->instance_bounding_box(j).min.z(); + } + } + } + + std::set> done; // keeps track of modified instances + + Selection::EMode selection_mode = m_selection.get_mode(); + + for (const GLVolume* v : m_volumes.volumes) { + const int object_idx = v->object_idx(); + if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) + continue; + + const int instance_idx = v->instance_idx(); + const int volume_idx = v->volume_idx(); + + done.insert(std::pair(object_idx, instance_idx)); + + // Rotate instances/volumes + ModelObject* model_object = m_model->objects[object_idx]; + if (model_object != nullptr) { + if (selection_mode == Selection::Instance) { +#if ENABLE_WORLD_COORDINATE + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); +#else + model_object->instances[instance_idx]->set_scaling_factor(v->get_instance_scaling_factor()); + model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); +#endif // ENABLE_WORLD_COORDINATE + } + else if (selection_mode == Selection::Volume) { +#if ENABLE_WORLD_COORDINATE + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); +#else + model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); + model_object->volumes[volume_idx]->set_scaling_factor(v->get_volume_scaling_factor()); + model_object->volumes[volume_idx]->set_offset(v->get_volume_offset()); +#endif // ENABLE_WORLD_COORDINATE + } + model_object->invalidate_bounding_box(); + } + } + + // Fixes sinking/flying instances + for (const std::pair& i : done) { + ModelObject* m = m_model->objects[i.first]; + const double shift_z = m->get_instance_min_z(i.second); + // leave sinking instances as sinking + if (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) { + const Vec3d shift(0.0, 0.0, -shift_z); + m_selection.translate(i.first, i.second, shift); + m->translate_instance(i.second, shift); + } + wxGetApp().obj_list()->update_info_items(static_cast(i.first)); + } + + if (!done.empty()) + post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED)); + + m_dirty = true; +} + +void GLCanvas3D::do_mirror(const std::string& snapshot_type) +{ + if (m_model == nullptr) + return; + + if (!snapshot_type.empty()) + wxGetApp().plater()->take_snapshot(_(snapshot_type)); + + // stores current min_z of instances + std::map, double> min_zs; + if (!snapshot_type.empty()) { + for (int i = 0; i < static_cast(m_model->objects.size()); ++i) { + const ModelObject* obj = m_model->objects[i]; + for (int j = 0; j < static_cast(obj->instances.size()); ++j) { + min_zs[{ i, j }] = obj->instance_bounding_box(j).min.z(); + } + } + } + + std::set> done; // keeps track of modified instances + + Selection::EMode selection_mode = m_selection.get_mode(); + + for (const GLVolume* v : m_volumes.volumes) { + int object_idx = v->object_idx(); + if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) + continue; + + int instance_idx = v->instance_idx(); + int volume_idx = v->volume_idx(); + + done.insert(std::pair(object_idx, instance_idx)); + + // Mirror instances/volumes + ModelObject* model_object = m_model->objects[object_idx]; + if (model_object != nullptr) { + if (selection_mode == Selection::Instance) +#if ENABLE_WORLD_COORDINATE + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); +#else + model_object->instances[instance_idx]->set_mirror(v->get_instance_mirror()); +#endif // ENABLE_WORLD_COORDINATE + else if (selection_mode == Selection::Volume) +#if ENABLE_WORLD_COORDINATE + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); +#else + model_object->volumes[volume_idx]->set_mirror(v->get_volume_mirror()); +#endif // ENABLE_WORLD_COORDINATE + + model_object->invalidate_bounding_box(); + } + } + + // Fixes sinking/flying instances + for (const std::pair& i : done) { + ModelObject* m = m_model->objects[i.first]; + double shift_z = m->get_instance_min_z(i.second); + // leave sinking instances as sinking + if (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) { + Vec3d shift(0.0, 0.0, -shift_z); + m_selection.translate(i.first, i.second, shift); + m->translate_instance(i.second, shift); + } + wxGetApp().obj_list()->update_info_items(static_cast(i.first)); + } + + post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + + m_dirty = true; +} + +#if ENABLE_WORLD_COORDINATE +void GLCanvas3D::do_reset_skew(const std::string& snapshot_type) +{ + if (m_model == nullptr) + return; + + if (!snapshot_type.empty()) + wxGetApp().plater()->take_snapshot(_(snapshot_type)); + + std::set> done; // keeps track of modified instances + + const Selection::IndicesList& idxs = m_selection.get_volume_idxs(); + + for (unsigned int id : idxs) { + const GLVolume* v = m_volumes.volumes[id]; + int object_idx = v->object_idx(); + if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) + continue; + + int instance_idx = v->instance_idx(); + int volume_idx = v->volume_idx(); + + done.insert(std::pair(object_idx, instance_idx)); + + ModelObject* model_object = m_model->objects[object_idx]; + if (model_object != nullptr) { + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); + model_object->invalidate_bounding_box(); + } + } + + post_event(SimpleEvent(EVT_GLCANVAS_RESET_SKEW)); + + m_dirty = true; +} +#endif // ENABLE_WORLD_COORDINATE + +void GLCanvas3D::update_gizmos_on_off_state() +{ + set_as_dirty(); + m_gizmos.update_data(); + m_gizmos.refresh_on_off_state(); +} + +void GLCanvas3D::handle_sidebar_focus_event(const std::string& opt_key, bool focus_on) +{ + m_sidebar_field = focus_on ? opt_key : ""; + if (!m_sidebar_field.empty()) + m_gizmos.reset_all_states(); + + m_dirty = true; +} + +void GLCanvas3D::handle_layers_data_focus_event(const t_layer_height_range range, const EditorType type) +{ + std::string field = "layer_" + std::to_string(type) + "_" + float_to_string_decimal_point(range.first) + "_" + float_to_string_decimal_point(range.second); + handle_sidebar_focus_event(field, true); +} + +void GLCanvas3D::update_ui_from_settings() +{ + m_dirty = true; + +#if __APPLE__ + // Update OpenGL scaling on OSX after the user toggled the "use_retina_opengl" settings in Preferences dialog. + const float orig_scaling = m_retina_helper->get_scale_factor(); + + const bool use_retina = wxGetApp().app_config->get("use_retina_opengl") == "1"; + BOOST_LOG_TRIVIAL(debug) << "GLCanvas3D: Use Retina OpenGL: " << use_retina; + m_retina_helper->set_use_retina(use_retina); + const float new_scaling = m_retina_helper->get_scale_factor(); + + if (new_scaling != orig_scaling) { + BOOST_LOG_TRIVIAL(debug) << "GLCanvas3D: Scaling factor: " << new_scaling; + + Camera& camera = wxGetApp().plater()->get_camera(); + camera.set_zoom(camera.get_zoom() * new_scaling / orig_scaling); + _refresh_if_shown_on_screen(); + } +#endif // ENABLE_RETINA_GL + + if (wxGetApp().is_editor()) + wxGetApp().plater()->enable_collapse_toolbar(wxGetApp().app_config->get("show_collapse_button") == "1"); +} + +GLCanvas3D::WipeTowerInfo GLCanvas3D::get_wipe_tower_info() const +{ + WipeTowerInfo wti; + + for (const GLVolume* vol : m_volumes.volumes) { + if (vol->is_wipe_tower) { + wti.m_pos = Vec2d(m_config->opt_float("wipe_tower_x"), + m_config->opt_float("wipe_tower_y")); + wti.m_rotation = (M_PI/180.) * m_config->opt_float("wipe_tower_rotation_angle"); + const BoundingBoxf3& bb = vol->bounding_box(); + wti.m_bb = BoundingBoxf{to_2d(bb.min), to_2d(bb.max)}; + break; + } + } + + return wti; +} + +Linef3 GLCanvas3D::mouse_ray(const Point& mouse_pos) +{ + float z0 = 0.0f; + float z1 = 1.0f; + return Linef3(_mouse_to_3d(mouse_pos, &z0), _mouse_to_3d(mouse_pos, &z1)); +} + +double GLCanvas3D::get_size_proportional_to_max_bed_size(double factor) const +{ + const BoundingBoxf& bbox = m_bed.build_volume().bounding_volume2d(); + return factor * std::max(bbox.size()[0], bbox.size()[1]); +} + +void GLCanvas3D::set_cursor(ECursorType type) +{ + if ((m_canvas != nullptr) && (m_cursor_type != type)) + { + switch (type) + { + case Standard: { m_canvas->SetCursor(*wxSTANDARD_CURSOR); break; } + case Cross: { m_canvas->SetCursor(*wxCROSS_CURSOR); break; } + } + + m_cursor_type = type; + } +} + +void GLCanvas3D::msw_rescale() +{ + m_gcode_viewer.invalidate_legend(); +} + +void GLCanvas3D::update_tooltip_for_settings_item_in_main_toolbar() +{ + std::string new_tooltip = _u8L("Switch to Settings") + + "\n" + "[" + GUI::shortkey_ctrl_prefix() + "2] - " + _u8L("Print Settings Tab") + + "\n" + "[" + GUI::shortkey_ctrl_prefix() + "3] - " + (current_printer_technology() == ptFFF ? _u8L("Filament Settings Tab") : _u8L("Material Settings Tab")) + + "\n" + "[" + GUI::shortkey_ctrl_prefix() + "4] - " + _u8L("Printer Settings Tab") ; + + m_main_toolbar.set_tooltip(get_main_toolbar_item_id("settings"), new_tooltip); +} + +bool GLCanvas3D::has_toolpaths_to_export() const +{ + return m_gcode_viewer.can_export_toolpaths(); +} + +void GLCanvas3D::export_toolpaths_to_obj(const char* filename) const +{ + m_gcode_viewer.export_toolpaths_to_obj(filename); +} + +void GLCanvas3D::mouse_up_cleanup() +{ + m_moving = false; + m_mouse.drag.move_volume_idx = -1; + m_mouse.set_start_position_3D_as_invalid(); + m_mouse.set_start_position_2D_as_invalid(); + m_mouse.dragging = false; + m_mouse.ignore_left_up = false; + m_dirty = true; + + if (m_canvas->HasCapture()) + m_canvas->ReleaseMouse(); +} + +void GLCanvas3D::update_sequential_clearance() +{ + if (current_printer_technology() != ptFFF || !fff_print()->config().complete_objects) + return; + + if (m_layers_editing.is_enabled() || m_gizmos.is_dragging()) + return; + + // collects instance transformations from volumes + // first define temporary cache + unsigned int instances_count = 0; + std::vector>> instance_transforms; + for (size_t obj = 0; obj < m_model->objects.size(); ++obj) { + instance_transforms.emplace_back(std::vector>()); + const ModelObject* model_object = m_model->objects[obj]; + for (size_t i = 0; i < model_object->instances.size(); ++i) { + instance_transforms[obj].emplace_back(std::optional()); + ++instances_count; + } + } + + if (instances_count == 1) + return; + + // second fill temporary cache with data from volumes + for (const GLVolume* v : m_volumes.volumes) { + if (v->is_modifier || v->is_wipe_tower) + continue; + + auto& transform = instance_transforms[v->object_idx()][v->instance_idx()]; + if (!transform.has_value()) + transform = v->get_instance_transformation(); + } + + // calculates objects 2d hulls (see also: Print::sequential_print_horizontal_clearance_valid()) + // this is done only the first time this method is called while moving the mouse, + // the results are then cached for following displacements + if (m_sequential_print_clearance_first_displacement) { + m_sequential_print_clearance.m_hull_2d_cache.clear(); + float shrink_factor = static_cast(scale_(0.5 * fff_print()->config().extruder_clearance_radius.value - EPSILON)); + double mitter_limit = scale_(0.1); + m_sequential_print_clearance.m_hull_2d_cache.reserve(m_model->objects.size()); + for (size_t i = 0; i < m_model->objects.size(); ++i) { + ModelObject* model_object = m_model->objects[i]; + ModelInstance* model_instance0 = model_object->instances.front(); + Polygon hull_2d = offset(model_object->convex_hull_2d(Geometry::assemble_transform({ 0.0, 0.0, model_instance0->get_offset().z() }, model_instance0->get_rotation(), + model_instance0->get_scaling_factor(), model_instance0->get_mirror())), + // Shrink the extruder_clearance_radius a tiny bit, so that if the object arrangement algorithm placed the objects + // exactly by satisfying the extruder_clearance_radius, this test will not trigger collision. + shrink_factor, + jtRound, mitter_limit).front(); + + Pointf3s& cache_hull_2d = m_sequential_print_clearance.m_hull_2d_cache.emplace_back(Pointf3s()); + cache_hull_2d.reserve(hull_2d.points.size()); + for (const Point& p : hull_2d.points) { + cache_hull_2d.emplace_back(unscale(p.x()), unscale(p.y()), 0.0); + } + } + m_sequential_print_clearance_first_displacement = false; + } + + // calculates instances 2d hulls (see also: Print::sequential_print_horizontal_clearance_valid()) + Polygons polygons; + polygons.reserve(instances_count); + for (size_t i = 0; i < instance_transforms.size(); ++i) { + const auto& instances = instance_transforms[i]; + double rotation_z0 = instances.front()->get_rotation().z(); + for (const auto& instance : instances) { + Geometry::Transformation transformation; + const Vec3d& offset = instance->get_offset(); + transformation.set_offset({ offset.x(), offset.y(), 0.0 }); + transformation.set_rotation(Z, instance->get_rotation().z() - rotation_z0); + const Transform3d& trafo = transformation.get_matrix(); + const Pointf3s& hull_2d = m_sequential_print_clearance.m_hull_2d_cache[i]; + Points inst_pts; + inst_pts.reserve(hull_2d.size()); + for (size_t j = 0; j < hull_2d.size(); ++j) { + const Vec3d p = trafo * hull_2d[j]; + inst_pts.emplace_back(scaled(p.x()), scaled(p.y())); + } + polygons.emplace_back(Geometry::convex_hull(std::move(inst_pts))); + } + } + + // sends instances 2d hulls to be rendered + set_sequential_print_clearance_visible(true); + set_sequential_print_clearance_render_fill(false); + set_sequential_print_clearance_polygons(polygons); +} + +bool GLCanvas3D::is_object_sinking(int object_idx) const +{ + for (const GLVolume* v : m_volumes.volumes) { + if (v->object_idx() == object_idx && (v->is_sinking() || (!v->is_modifier && v->is_below_printbed()))) + return true; + } + return false; +} + +bool GLCanvas3D::_is_shown_on_screen() const +{ + return (m_canvas != nullptr) ? m_canvas->IsShownOnScreen() : false; +} + +// Getter for the const char*[] +static bool string_getter(const bool is_undo, int idx, const char** out_text) +{ + return wxGetApp().plater()->undo_redo_string_getter(is_undo, idx, out_text); +} + +bool GLCanvas3D::_render_undo_redo_stack(const bool is_undo, float pos_x) +{ + bool action_taken = false; + + ImGuiWrapper* imgui = wxGetApp().imgui(); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + imgui->set_next_window_pos(pos_x, m_undoredo_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); +#else + const float x = pos_x * (float)wxGetApp().plater()->get_camera().get_zoom() + 0.5f * (float)get_canvas_size().get_width(); + imgui->set_next_window_pos(x, m_undoredo_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + std::string title = is_undo ? L("Undo History") : L("Redo History"); + imgui->begin(_(title), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + + int hovered = m_imgui_undo_redo_hovered_pos; + int selected = -1; + float em = static_cast(wxGetApp().em_unit()); +#if ENABLE_RETINA_GL + em *= m_retina_helper->get_scale_factor(); +#endif + + if (imgui->undo_redo_list(ImVec2(18 * em, 26 * em), is_undo, &string_getter, hovered, selected, m_mouse_wheel)) + m_imgui_undo_redo_hovered_pos = hovered; + else + m_imgui_undo_redo_hovered_pos = -1; + + if (selected >= 0) { + is_undo ? wxGetApp().plater()->undo_to(selected) : wxGetApp().plater()->redo_to(selected); + action_taken = true; + } + + imgui->text(wxString::Format(is_undo ? _L_PLURAL("Undo %1$d Action", "Undo %1$d Actions", hovered + 1) : _L_PLURAL("Redo %1$d Action", "Redo %1$d Actions", hovered + 1), hovered + 1)); + + imgui->end(); + + return action_taken; +} + +// Getter for the const char*[] for the search list +static bool search_string_getter(int idx, const char** label, const char** tooltip) +{ + return wxGetApp().plater()->search_string_getter(idx, label, tooltip); +} + +bool GLCanvas3D::_render_search_list(float pos_x) +{ + bool action_taken = false; + ImGuiWrapper* imgui = wxGetApp().imgui(); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + imgui->set_next_window_pos(pos_x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); +#else + const float x = /*pos_x * (float)wxGetApp().plater()->get_camera().get_zoom() + */0.5f * (float)get_canvas_size().get_width(); + imgui->set_next_window_pos(x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + std::string title = L("Search"); + imgui->begin(_(title), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + + int selected = -1; + bool edited = false; + float em = static_cast(wxGetApp().em_unit()); +#if ENABLE_RETINA_GL + em *= m_retina_helper->get_scale_factor(); +#endif // ENABLE_RETINA_GL + + Sidebar& sidebar = wxGetApp().sidebar(); + + std::string& search_line = sidebar.get_search_line(); + char *s = new char[255]; + strcpy(s, search_line.empty() ? _u8L("Enter a search term").c_str() : search_line.c_str()); + + imgui->search_list(ImVec2(45 * em, 30 * em), &search_string_getter, s, + sidebar.get_searcher().view_params, + selected, edited, m_mouse_wheel, wxGetApp().is_localized()); + + search_line = s; + delete [] s; + if (search_line == _u8L("Enter a search term")) + search_line.clear(); + + if (edited) + sidebar.search(); + + if (selected >= 0) { + // selected == 9999 means that Esc kye was pressed + /*// revert commit https://github.com/prusa3d/PrusaSlicer/commit/91897589928789b261ca0dc735ffd46f2b0b99f2 + if (selected == 9999) + action_taken = true; + else + sidebar.jump_to_option(selected);*/ + if (selected != 9999) { + imgui->end(); // end imgui before the jump to option + sidebar.jump_to_option(selected); + return true; + } + action_taken = true; + } + + imgui->end(); + + return action_taken; +} + +bool GLCanvas3D::_render_arrange_menu(float pos_x) +{ + ImGuiWrapper *imgui = wxGetApp().imgui(); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + imgui->set_next_window_pos(pos_x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); +#else + auto canvas_w = float(get_canvas_size().get_width()); + const float x = pos_x * float(wxGetApp().plater()->get_camera().get_zoom()) + 0.5f * canvas_w; + imgui->set_next_window_pos(x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + imgui->begin(_L("Arrange options"), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); + + ArrangeSettings settings = get_arrange_settings(); + ArrangeSettings &settings_out = get_arrange_settings(); + + auto &appcfg = wxGetApp().app_config; + PrinterTechnology ptech = current_printer_technology(); + + bool settings_changed = false; + float dist_min = 0.f; + float dist_bed_min = 0.f; + std::string dist_key = "min_object_distance"; + std::string dist_bed_key = "min_bed_distance"; + std::string rot_key = "enable_rotation"; + std::string postfix; + + if (ptech == ptSLA) { + postfix = "_sla"; + } else if (ptech == ptFFF) { + auto co_opt = m_config->option("complete_objects"); + if (co_opt && co_opt->value) { + dist_min = float(min_object_distance(*m_config)); + postfix = "_fff_seq_print"; + } else { + dist_min = 0.f; + postfix = "_fff"; + } + } + + dist_key += postfix; + dist_bed_key += postfix; + rot_key += postfix; + + imgui->text(GUI::format_wxstr(_L("Press %1%left mouse button to enter the exact value"), shortkey_ctrl_prefix())); + + if (imgui->slider_float(_L("Spacing"), &settings.distance, dist_min, 100.0f, "%5.2f") || dist_min > settings.distance) { + settings.distance = std::max(dist_min, settings.distance); + settings_out.distance = settings.distance; + appcfg->set("arrange", dist_key.c_str(), float_to_string_decimal_point(settings_out.distance)); + settings_changed = true; + } + + if (imgui->slider_float(_L("Spacing from bed"), &settings.distance_from_bed, dist_bed_min, 100.0f, "%5.2f") || dist_bed_min > settings.distance_from_bed) { + settings.distance_from_bed = std::max(dist_bed_min, settings.distance_from_bed); + settings_out.distance_from_bed = settings.distance_from_bed; + appcfg->set("arrange", dist_bed_key.c_str(), float_to_string_decimal_point(settings_out.distance_from_bed)); + settings_changed = true; + } + + if (imgui->checkbox(_L("Enable rotations (slow)"), settings.enable_rotation)) { + settings_out.enable_rotation = settings.enable_rotation; + appcfg->set("arrange", rot_key.c_str(), settings_out.enable_rotation? "1" : "0"); + settings_changed = true; + } + + ImGui::Separator(); + + if (imgui->button(_L("Reset"))) { + settings_out = ArrangeSettings{}; + settings_out.distance = std::max(dist_min, settings_out.distance); + appcfg->set("arrange", dist_key.c_str(), float_to_string_decimal_point(settings_out.distance)); + appcfg->set("arrange", dist_bed_key.c_str(), float_to_string_decimal_point(settings_out.distance_from_bed)); + appcfg->set("arrange", rot_key.c_str(), settings_out.enable_rotation? "1" : "0"); + settings_changed = true; + } + + ImGui::SameLine(); + + if (imgui->button(_L("Arrange"))) { + wxGetApp().plater()->arrange(); + } + + imgui->end(); + + return settings_changed; +} + +#define ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT 0 +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT +static void debug_output_thumbnail(const ThumbnailData& thumbnail_data) +{ + // debug export of generated image + wxImage image(thumbnail_data.width, thumbnail_data.height); + image.InitAlpha(); + + for (unsigned int r = 0; r < thumbnail_data.height; ++r) + { + unsigned int rr = (thumbnail_data.height - 1 - r) * thumbnail_data.width; + for (unsigned int c = 0; c < thumbnail_data.width; ++c) + { + unsigned char* px = (unsigned char*)thumbnail_data.pixels.data() + 4 * (rr + c); + image.SetRGB((int)c, (int)r, px[0], px[1], px[2]); + image.SetAlpha((int)c, (int)r, px[3]); + } + } + + image.SaveFile("C:/prusa/test/test.png", wxBITMAP_TYPE_PNG); +} +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT + +void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type) +{ + auto is_visible = [](const GLVolume& v) { + bool ret = v.printable; + ret &= (!v.shader_outside_printer_detection_enabled || !v.is_outside); + return ret; + }; + + GLVolumePtrs visible_volumes; + + for (GLVolume* vol : volumes.volumes) { + if (!vol->is_modifier && !vol->is_wipe_tower && (!thumbnail_params.parts_only || vol->composite_id.volume_id >= 0)) { + if (!thumbnail_params.printable_only || is_visible(*vol)) + visible_volumes.emplace_back(vol); + } + } + + BoundingBoxf3 volumes_box; + if (!visible_volumes.empty()) { + for (const GLVolume* vol : visible_volumes) { + volumes_box.merge(vol->transformed_bounding_box()); + } + } + else + // This happens for empty projects + volumes_box = m_bed.extended_bounding_box(); + + Camera camera; + camera.set_type(camera_type); + camera.set_scene_box(scene_bounding_box()); +#if ENABLE_RAYCAST_PICKING + camera.set_viewport(0, 0, thumbnail_data.width, thumbnail_data.height); + camera.apply_viewport(); +#else + camera.apply_viewport(0, 0, thumbnail_data.width, thumbnail_data.height); +#endif // ENABLE_RAYCAST_PICKING + camera.zoom_to_box(volumes_box); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Transform3d& view_matrix = camera.get_view_matrix(); +#else + camera.apply_view_matrix(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + double near_z = -1.0; + double far_z = -1.0; + + if (thumbnail_params.show_bed) { + // extends the near and far z of the frustrum to avoid the bed being clipped + + // box in eye space +#if ENABLE_LEGACY_OPENGL_REMOVAL + const BoundingBoxf3 t_bed_box = m_bed.extended_bounding_box().transformed(view_matrix); +#else + const BoundingBoxf3 t_bed_box = m_bed.extended_bounding_box().transformed(camera.get_view_matrix()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + near_z = -t_bed_box.max.z(); + far_z = -t_bed_box.min.z(); + } + + camera.apply_projection(volumes_box, near_z, far_z); + + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + + if (thumbnail_params.transparent_background) + glsafe(::glClearColor(0.0f, 0.0f, 0.0f, 0.0f)); + + glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); + glsafe(::glEnable(GL_DEPTH_TEST)); + + shader->start_using(); + shader->set_uniform("emission_factor", 0.0f); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Transform3d& projection_matrix = camera.get_projection_matrix(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + for (GLVolume* vol : visible_volumes) { +#if ENABLE_LEGACY_OPENGL_REMOVAL + vol->model.set_color((vol->printable && !vol->is_outside) ? (current_printer_technology() == ptSLA ? vol->color : ColorRGBA::ORANGE()) : ColorRGBA::GRAY()); +#else + shader->set_uniform("uniform_color", (vol->printable && !vol->is_outside) ? (current_printer_technology() == ptSLA ? vol->color : ColorRGBA::ORANGE()) : ColorRGBA::GRAY()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + // the volume may have been deactivated by an active gizmo + const bool is_active = vol->is_active; + vol->is_active = true; +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Transform3d model_matrix = vol->world_matrix(); + shader->set_uniform("view_model_matrix", view_matrix * model_matrix); + shader->set_uniform("projection_matrix", projection_matrix); + const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader->set_uniform("view_normal_matrix", view_normal_matrix); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + vol->render(); + vol->is_active = is_active; + } + + shader->stop_using(); + + glsafe(::glDisable(GL_DEPTH_TEST)); + + if (thumbnail_params.show_bed) +#if ENABLE_LEGACY_OPENGL_REMOVAL + _render_bed(view_matrix, projection_matrix, !camera.is_looking_downward(), false); +#else + _render_bed(!camera.is_looking_downward(), false); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + // restore background color + if (thumbnail_params.transparent_background) + glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 1.0f)); +} + +void GLCanvas3D::_render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type) +{ + thumbnail_data.set(w, h); + if (!thumbnail_data.is_valid()) + return; + + bool multisample = m_multisample_allowed; + if (multisample) + glsafe(::glEnable(GL_MULTISAMPLE)); + + GLint max_samples; + glsafe(::glGetIntegerv(GL_MAX_SAMPLES, &max_samples)); + GLsizei num_samples = max_samples / 2; + + GLuint render_fbo; + glsafe(::glGenFramebuffers(1, &render_fbo)); + glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, render_fbo)); + + GLuint render_tex = 0; + GLuint render_tex_buffer = 0; + if (multisample) { + // use renderbuffer instead of texture to avoid the need to use glTexImage2DMultisample which is available only since OpenGL 3.2 + glsafe(::glGenRenderbuffers(1, &render_tex_buffer)); + glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, render_tex_buffer)); + glsafe(::glRenderbufferStorageMultisample(GL_RENDERBUFFER, num_samples, GL_RGBA8, w, h)); + glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, render_tex_buffer)); + } + else { + glsafe(::glGenTextures(1, &render_tex)); + glsafe(::glBindTexture(GL_TEXTURE_2D, render_tex)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, render_tex, 0)); + } + + GLuint render_depth; + glsafe(::glGenRenderbuffers(1, &render_depth)); + glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, render_depth)); + if (multisample) + glsafe(::glRenderbufferStorageMultisample(GL_RENDERBUFFER, num_samples, GL_DEPTH_COMPONENT24, w, h)); + else + glsafe(::glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, w, h)); + + glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, render_depth)); + + GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 }; + glsafe(::glDrawBuffers(1, drawBufs)); + + if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { + _render_thumbnail_internal(thumbnail_data, thumbnail_params, volumes, camera_type); + + if (multisample) { + GLuint resolve_fbo; + glsafe(::glGenFramebuffers(1, &resolve_fbo)); + glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, resolve_fbo)); + + GLuint resolve_tex; + glsafe(::glGenTextures(1, &resolve_tex)); + glsafe(::glBindTexture(GL_TEXTURE_2D, resolve_tex)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, resolve_tex, 0)); + + glsafe(::glDrawBuffers(1, drawBufs)); + + if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { + glsafe(::glBindFramebuffer(GL_READ_FRAMEBUFFER, render_fbo)); + glsafe(::glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolve_fbo)); + glsafe(::glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_LINEAR)); + + glsafe(::glBindFramebuffer(GL_READ_FRAMEBUFFER, resolve_fbo)); + glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data())); + } + + glsafe(::glDeleteTextures(1, &resolve_tex)); + glsafe(::glDeleteFramebuffers(1, &resolve_fbo)); + } + else + glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data())); + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT + debug_output_thumbnail(thumbnail_data); +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT + } + + glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, 0)); + glsafe(::glDeleteRenderbuffers(1, &render_depth)); + if (render_tex_buffer != 0) + glsafe(::glDeleteRenderbuffers(1, &render_tex_buffer)); + if (render_tex != 0) + glsafe(::glDeleteTextures(1, &render_tex)); + glsafe(::glDeleteFramebuffers(1, &render_fbo)); + + if (multisample) + glsafe(::glDisable(GL_MULTISAMPLE)); +} + +void GLCanvas3D::_render_thumbnail_framebuffer_ext(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type) +{ + thumbnail_data.set(w, h); + if (!thumbnail_data.is_valid()) + return; + + bool multisample = m_multisample_allowed; + if (multisample) + glsafe(::glEnable(GL_MULTISAMPLE)); + + GLint max_samples; + glsafe(::glGetIntegerv(GL_MAX_SAMPLES_EXT, &max_samples)); + GLsizei num_samples = max_samples / 2; + + GLuint render_fbo; + glsafe(::glGenFramebuffersEXT(1, &render_fbo)); + glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, render_fbo)); + + GLuint render_tex = 0; + GLuint render_tex_buffer = 0; + if (multisample) { + // use renderbuffer instead of texture to avoid the need to use glTexImage2DMultisample which is available only since OpenGL 3.2 + glsafe(::glGenRenderbuffersEXT(1, &render_tex_buffer)); + glsafe(::glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, render_tex_buffer)); + glsafe(::glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER_EXT, num_samples, GL_RGBA8, w, h)); + glsafe(::glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_RENDERBUFFER_EXT, render_tex_buffer)); + } + else { + glsafe(::glGenTextures(1, &render_tex)); + glsafe(::glBindTexture(GL_TEXTURE_2D, render_tex)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, render_tex, 0)); + } + + GLuint render_depth; + glsafe(::glGenRenderbuffersEXT(1, &render_depth)); + glsafe(::glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, render_depth)); + if (multisample) + glsafe(::glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER_EXT, num_samples, GL_DEPTH_COMPONENT24, w, h)); + else + glsafe(::glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, w, h)); + + glsafe(::glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, render_depth)); + + GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 }; + glsafe(::glDrawBuffers(1, drawBufs)); + + if (::glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) == GL_FRAMEBUFFER_COMPLETE_EXT) { + _render_thumbnail_internal(thumbnail_data, thumbnail_params, volumes, camera_type); + + if (multisample) { + GLuint resolve_fbo; + glsafe(::glGenFramebuffersEXT(1, &resolve_fbo)); + glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, resolve_fbo)); + + GLuint resolve_tex; + glsafe(::glGenTextures(1, &resolve_tex)); + glsafe(::glBindTexture(GL_TEXTURE_2D, resolve_tex)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, resolve_tex, 0)); + + glsafe(::glDrawBuffers(1, drawBufs)); + + if (::glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) == GL_FRAMEBUFFER_COMPLETE_EXT) { + glsafe(::glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, render_fbo)); + glsafe(::glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, resolve_fbo)); + glsafe(::glBlitFramebufferEXT(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_LINEAR)); + + glsafe(::glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, resolve_fbo)); + glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data())); + } + + glsafe(::glDeleteTextures(1, &resolve_tex)); + glsafe(::glDeleteFramebuffersEXT(1, &resolve_fbo)); + } + else + glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data())); + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT + debug_output_thumbnail(thumbnail_data); +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT + } + + glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0)); + glsafe(::glDeleteRenderbuffersEXT(1, &render_depth)); + if (render_tex_buffer != 0) + glsafe(::glDeleteRenderbuffersEXT(1, &render_tex_buffer)); + if (render_tex != 0) + glsafe(::glDeleteTextures(1, &render_tex)); + glsafe(::glDeleteFramebuffersEXT(1, &render_fbo)); + + if (multisample) + glsafe(::glDisable(GL_MULTISAMPLE)); +} + +void GLCanvas3D::_render_thumbnail_legacy(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type) +{ + // check that thumbnail size does not exceed the default framebuffer size + const Size& cnv_size = get_canvas_size(); + unsigned int cnv_w = (unsigned int)cnv_size.get_width(); + unsigned int cnv_h = (unsigned int)cnv_size.get_height(); + if (w > cnv_w || h > cnv_h) { + float ratio = std::min((float)cnv_w / (float)w, (float)cnv_h / (float)h); + w = (unsigned int)(ratio * (float)w); + h = (unsigned int)(ratio * (float)h); + } + + thumbnail_data.set(w, h); + if (!thumbnail_data.is_valid()) + return; + + _render_thumbnail_internal(thumbnail_data, thumbnail_params, volumes, camera_type); + + glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data())); +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT + debug_output_thumbnail(thumbnail_data); +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT + + // restore the default framebuffer size to avoid flickering on the 3D scene +#if ENABLE_RAYCAST_PICKING + wxGetApp().plater()->get_camera().apply_viewport(); +#else + wxGetApp().plater()->get_camera().apply_viewport(0, 0, cnv_size.get_width(), cnv_size.get_height()); +#endif // ENABLE_RAYCAST_PICKING +} + +bool GLCanvas3D::_init_toolbars() +{ + if (!_init_main_toolbar()) + return false; + + if (!_init_undoredo_toolbar()) + return false; + + if (!_init_view_toolbar()) + return false; + + if (!_init_collapse_toolbar()) + return false; + + return true; +} + +bool GLCanvas3D::_init_main_toolbar() +{ + if (!m_main_toolbar.is_enabled()) + return true; + + BackgroundTexture::Metadata background_data; + background_data.filename = "toolbar_background.png"; + background_data.left = 16; + background_data.top = 16; + background_data.right = 16; + background_data.bottom = 16; + + if (!m_main_toolbar.init(background_data)) { + // unable to init the toolbar texture, disable it + m_main_toolbar.set_enabled(false); + return true; + } + // init arrow +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!m_main_toolbar.init_arrow("toolbar_arrow_2.svg")) +#else + BackgroundTexture::Metadata arrow_data; + arrow_data.filename = "toolbar_arrow.svg"; + arrow_data.left = 0; + arrow_data.top = 0; + arrow_data.right = 0; + arrow_data.bottom = 0; + if (!m_main_toolbar.init_arrow(arrow_data)) +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + BOOST_LOG_TRIVIAL(error) << "Main toolbar failed to load arrow texture."; + + // m_gizmos is created at constructor, thus we can init arrow here. +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!m_gizmos.init_arrow("toolbar_arrow_2.svg")) +#else + if (!m_gizmos.init_arrow(arrow_data)) +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + BOOST_LOG_TRIVIAL(error) << "Gizmos manager failed to load arrow texture."; + +// m_main_toolbar.set_layout_type(GLToolbar::Layout::Vertical); + m_main_toolbar.set_layout_type(GLToolbar::Layout::Horizontal); + m_main_toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Right); + m_main_toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Top); + m_main_toolbar.set_border(5.0f); + m_main_toolbar.set_separator_size(5); + m_main_toolbar.set_gap_size(4); + + GLToolbarItem::Data item; + + item.name = "add"; + item.icon_filename = "add.svg"; + item.tooltip = _utf8(L("Add...")) + " [" + GUI::shortkey_ctrl_prefix() + "I]"; + item.sprite_id = 0; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_ADD)); }; + if (!m_main_toolbar.add_item(item)) + return false; + + item.name = "delete"; + item.icon_filename = "remove.svg"; + item.tooltip = _utf8(L("Delete")) + " [Del]"; + item.sprite_id = 1; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_DELETE)); }; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_delete(); }; + if (!m_main_toolbar.add_item(item)) + return false; + + item.name = "deleteall"; + item.icon_filename = "delete_all.svg"; + item.tooltip = _utf8(L("Delete all")) + " [" + GUI::shortkey_ctrl_prefix() + "Del]"; + item.sprite_id = 2; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_DELETE_ALL)); }; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_delete_all(); }; + if (!m_main_toolbar.add_item(item)) + return false; + + item.name = "arrange"; + item.icon_filename = "arrange.svg"; + item.tooltip = _utf8(L("Arrange")) + " [A]\n" + _utf8(L("Arrange selection")) + " [Shift+A]\n" + _utf8(L("Click right mouse button to show arrangement options")); + item.sprite_id = 3; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_ARRANGE)); }; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_arrange(); }; + item.right.toggable = true; + item.right.render_callback = [this](float left, float right, float, float) { + if (m_canvas != nullptr) + _render_arrange_menu(0.5f * (left + right)); + }; + if (!m_main_toolbar.add_item(item)) + return false; + + item.right.toggable = false; + item.right.render_callback = GLToolbarItem::Default_Render_Callback; + + if (!m_main_toolbar.add_separator()) + return false; + + item.name = "copy"; + item.icon_filename = "copy.svg"; + item.tooltip = _utf8(L("Copy")) + " [" + GUI::shortkey_ctrl_prefix() + "C]"; + item.sprite_id = 4; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_COPY)); }; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_copy_to_clipboard(); }; + if (!m_main_toolbar.add_item(item)) + return false; + + item.name = "paste"; + item.icon_filename = "paste.svg"; + item.tooltip = _utf8(L("Paste")) + " [" + GUI::shortkey_ctrl_prefix() + "V]"; + item.sprite_id = 5; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_PASTE)); }; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_paste_from_clipboard(); }; + if (!m_main_toolbar.add_item(item)) + return false; + + if (!m_main_toolbar.add_separator()) + return false; + + item.name = "more"; + item.icon_filename = "instance_add.svg"; + item.tooltip = _utf8(L("Add instance")) + " [+]"; + item.sprite_id = 6; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_MORE)); }; + item.visibility_callback = []()->bool { return wxGetApp().get_mode() != comSimple; }; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_increase_instances(); }; + + if (!m_main_toolbar.add_item(item)) + return false; + + item.name = "fewer"; + item.icon_filename = "instance_remove.svg"; + item.tooltip = _utf8(L("Remove instance")) + " [-]"; + item.sprite_id = 7; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_FEWER)); }; + item.visibility_callback = []()->bool { return wxGetApp().get_mode() != comSimple; }; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_decrease_instances(); }; + if (!m_main_toolbar.add_item(item)) + return false; + + if (!m_main_toolbar.add_separator()) + return false; + + item.name = "splitobjects"; + item.icon_filename = "split_objects.svg"; + item.tooltip = _utf8(L("Split to objects")); + item.sprite_id = 8; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_SPLIT_OBJECTS)); }; + item.visibility_callback = GLToolbarItem::Default_Visibility_Callback; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_split_to_objects(); }; + if (!m_main_toolbar.add_item(item)) + return false; + + item.name = "splitvolumes"; + item.icon_filename = "split_parts.svg"; + item.tooltip = _utf8(L("Split to parts")); + item.sprite_id = 9; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_SPLIT_VOLUMES)); }; + item.visibility_callback = []()->bool { return wxGetApp().get_mode() != comSimple; }; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_split_to_volumes(); }; + if (!m_main_toolbar.add_item(item)) + return false; + + if (!m_main_toolbar.add_separator()) + return false; + + item.name = "settings"; + item.icon_filename = "settings.svg"; + item.tooltip = _u8L("Switch to Settings") + "\n" + "[" + GUI::shortkey_ctrl_prefix() + "2] - " + _u8L("Print Settings Tab") + + "\n" + "[" + GUI::shortkey_ctrl_prefix() + "3] - " + (current_printer_technology() == ptFFF ? _u8L("Filament Settings Tab") : _u8L("Material Settings Tab")) + + "\n" + "[" + GUI::shortkey_ctrl_prefix() + "4] - " + _u8L("Printer Settings Tab") ; + item.sprite_id = 10; + item.enabling_callback = GLToolbarItem::Default_Enabling_Callback; + item.visibility_callback = []() { return (wxGetApp().app_config->get("new_settings_layout_mode") == "1" || + wxGetApp().app_config->get("dlg_settings_layout_mode") == "1"); }; + item.left.action_callback = []() { wxGetApp().mainframe->select_tab(); }; + if (!m_main_toolbar.add_item(item)) + return false; + + /* + if (!m_main_toolbar.add_separator()) + return false; + */ + + item.name = "search"; + item.icon_filename = "search_.svg"; + item.tooltip = _utf8(L("Search")) + " [" + GUI::shortkey_ctrl_prefix() + "F]"; + item.sprite_id = 11; + item.left.toggable = true; + item.left.render_callback = [this](float left, float right, float, float) { + if (m_canvas != nullptr) { + if (_render_search_list(0.5f * (left + right))) + _deactivate_search_toolbar_item(); + } + }; + item.left.action_callback = GLToolbarItem::Default_Action_Callback; + item.visibility_callback = GLToolbarItem::Default_Visibility_Callback; + item.enabling_callback = GLToolbarItem::Default_Enabling_Callback; + if (!m_main_toolbar.add_item(item)) + return false; + + if (!m_main_toolbar.add_separator()) + return false; + + item.name = "layersediting"; + item.icon_filename = "layers_white.svg"; + item.tooltip = _utf8(L("Variable layer height")); + item.sprite_id = 12; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_LAYERSEDITING)); }; + item.visibility_callback = [this]()->bool { + bool res = current_printer_technology() == ptFFF; + // turns off if changing printer technology + if (!res && m_main_toolbar.is_item_visible("layersediting") && m_main_toolbar.is_item_pressed("layersediting")) + force_main_toolbar_left_action(get_main_toolbar_item_id("layersediting")); + + return res; + }; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_layers_editing(); }; + item.left.render_callback = GLToolbarItem::Default_Render_Callback; + if (!m_main_toolbar.add_item(item)) + return false; + + return true; +} + +bool GLCanvas3D::_init_undoredo_toolbar() +{ + if (!m_undoredo_toolbar.is_enabled()) + return true; + + BackgroundTexture::Metadata background_data; + background_data.filename = "toolbar_background.png"; + background_data.left = 16; + background_data.top = 16; + background_data.right = 16; + background_data.bottom = 16; + + if (!m_undoredo_toolbar.init(background_data)) { + // unable to init the toolbar texture, disable it + m_undoredo_toolbar.set_enabled(false); + return true; + } + + // init arrow +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!m_undoredo_toolbar.init_arrow("toolbar_arrow_2.svg")) +#else + BackgroundTexture::Metadata arrow_data; + arrow_data.filename = "toolbar_arrow.svg"; + arrow_data.left = 0; + arrow_data.top = 0; + arrow_data.right = 0; + arrow_data.bottom = 0; + if (!m_undoredo_toolbar.init_arrow(arrow_data)) +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + BOOST_LOG_TRIVIAL(error) << "Undo/Redo toolbar failed to load arrow texture."; + +// m_undoredo_toolbar.set_layout_type(GLToolbar::Layout::Vertical); + m_undoredo_toolbar.set_layout_type(GLToolbar::Layout::Horizontal); + m_undoredo_toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Left); + m_undoredo_toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Top); + m_undoredo_toolbar.set_border(5.0f); + m_undoredo_toolbar.set_separator_size(5); + m_undoredo_toolbar.set_gap_size(4); + + GLToolbarItem::Data item; + + item.name = "undo"; + item.icon_filename = "undo_toolbar.svg"; + item.tooltip = _utf8(L("Undo")) + " [" + GUI::shortkey_ctrl_prefix() + "Z]\n" + _utf8(L("Click right mouse button to open/close History")); + item.sprite_id = 0; + item.left.action_callback = [this]() { post_event(SimpleEvent(EVT_GLCANVAS_UNDO)); }; + item.right.toggable = true; + item.right.action_callback = [this]() { m_imgui_undo_redo_hovered_pos = -1; }; + item.right.render_callback = [this](float left, float right, float, float) { + if (m_canvas != nullptr) { + if (_render_undo_redo_stack(true, 0.5f * (left + right))) + _deactivate_undo_redo_toolbar_items(); + } + }; + item.enabling_callback = [this]()->bool { + bool can_undo = wxGetApp().plater()->can_undo(); + int id = m_undoredo_toolbar.get_item_id("undo"); + + std::string curr_additional_tooltip; + m_undoredo_toolbar.get_additional_tooltip(id, curr_additional_tooltip); + + std::string new_additional_tooltip; + if (can_undo) { + std::string action; + wxGetApp().plater()->undo_redo_topmost_string_getter(true, action); + new_additional_tooltip = (boost::format(_utf8(L("Next Undo action: %1%"))) % action).str(); + } + + if (new_additional_tooltip != curr_additional_tooltip) { + m_undoredo_toolbar.set_additional_tooltip(id, new_additional_tooltip); + set_tooltip(""); + } + return can_undo; + }; + + if (!m_undoredo_toolbar.add_item(item)) + return false; + + item.name = "redo"; + item.icon_filename = "redo_toolbar.svg"; + item.tooltip = _utf8(L("Redo")) + " [" + GUI::shortkey_ctrl_prefix() + "Y]\n" + _utf8(L("Click right mouse button to open/close History")); + item.sprite_id = 1; + item.left.action_callback = [this]() { post_event(SimpleEvent(EVT_GLCANVAS_REDO)); }; + item.right.action_callback = [this]() { m_imgui_undo_redo_hovered_pos = -1; }; + item.right.render_callback = [this](float left, float right, float, float) { + if (m_canvas != nullptr) { + if (_render_undo_redo_stack(false, 0.5f * (left + right))) + _deactivate_undo_redo_toolbar_items(); + } + }; + item.enabling_callback = [this]()->bool { + bool can_redo = wxGetApp().plater()->can_redo(); + int id = m_undoredo_toolbar.get_item_id("redo"); + + std::string curr_additional_tooltip; + m_undoredo_toolbar.get_additional_tooltip(id, curr_additional_tooltip); + + std::string new_additional_tooltip; + if (can_redo) { + std::string action; + wxGetApp().plater()->undo_redo_topmost_string_getter(false, action); + new_additional_tooltip = (boost::format(_utf8(L("Next Redo action: %1%"))) % action).str(); + } + + if (new_additional_tooltip != curr_additional_tooltip) { + m_undoredo_toolbar.set_additional_tooltip(id, new_additional_tooltip); + set_tooltip(""); + } + return can_redo; + }; + + if (!m_undoredo_toolbar.add_item(item)) + return false; + /* + if (!m_undoredo_toolbar.add_separator()) + return false; + */ + return true; +} + +bool GLCanvas3D::_init_view_toolbar() +{ + return wxGetApp().plater()->init_view_toolbar(); +} + +bool GLCanvas3D::_init_collapse_toolbar() +{ + return wxGetApp().plater()->init_collapse_toolbar(); +} + +bool GLCanvas3D::_set_current() +{ + return m_context != nullptr && m_canvas->SetCurrent(*m_context); +} + +void GLCanvas3D::_resize(unsigned int w, unsigned int h) +{ + if (m_canvas == nullptr && m_context == nullptr) + return; + + const std::array new_size = { w, h }; + if (m_old_size == new_size) + return; + + m_old_size = new_size; + + auto *imgui = wxGetApp().imgui(); + imgui->set_display_size(static_cast(w), static_cast(h)); + const float font_size = 1.5f * wxGetApp().em_unit(); +#if ENABLE_RETINA_GL + imgui->set_scaling(font_size, 1.0f, m_retina_helper->get_scale_factor()); +#else + imgui->set_scaling(font_size, m_canvas->GetContentScaleFactor(), 1.0f); +#endif + + this->request_extra_frame(); + + // ensures that this canvas is current + _set_current(); +} + +BoundingBoxf3 GLCanvas3D::_max_bounding_box(bool include_gizmos, bool include_bed_model) const +{ + BoundingBoxf3 bb = volumes_bounding_box(); + + // The following is a workaround for gizmos not being taken in account when calculating the tight camera frustrum + // A better solution would ask the gizmo manager for the bounding box of the current active gizmo, if any + if (include_gizmos && m_gizmos.is_running()) { + const BoundingBoxf3 sel_bb = m_selection.get_bounding_box(); + const Vec3d sel_bb_center = sel_bb.center(); + const Vec3d extend_by = sel_bb.max_size() * Vec3d::Ones(); + bb.merge(BoundingBoxf3(sel_bb_center - extend_by, sel_bb_center + extend_by)); + } + + const BoundingBoxf3 bed_bb = include_bed_model ? m_bed.extended_bounding_box() : m_bed.build_volume().bounding_volume(); + bb.merge(bed_bb); + + if (!m_main_toolbar.is_enabled()) + bb.merge(m_gcode_viewer.get_max_bounding_box()); + + // clamp max bb size with respect to bed bb size + if (!m_picking_enabled) { + static const double max_scale_factor = 2.0; + const Vec3d bb_size = bb.size(); + const Vec3d bed_bb_size = m_bed.build_volume().bounding_volume().size(); + + if ((bed_bb_size.x() > 0.0 && bb_size.x() > max_scale_factor * bed_bb_size.x()) || + (bed_bb_size.y() > 0.0 && bb_size.y() > max_scale_factor * bed_bb_size.y()) || + (bed_bb_size.z() > 0.0 && bb_size.z() > max_scale_factor * bed_bb_size.z())) { + const Vec3d bed_bb_center = bed_bb.center(); + const Vec3d extend_by = max_scale_factor * bed_bb_size; + bb = BoundingBoxf3(bed_bb_center - extend_by, bed_bb_center + extend_by); + } + } + + return bb; +} + +void GLCanvas3D::_zoom_to_box(const BoundingBoxf3& box, double margin_factor) +{ + wxGetApp().plater()->get_camera().zoom_to_box(box, margin_factor); + m_dirty = true; +} + +void GLCanvas3D::_update_camera_zoom(double zoom) +{ + wxGetApp().plater()->get_camera().update_zoom(zoom); + m_dirty = true; +} + +void GLCanvas3D::_refresh_if_shown_on_screen() +{ + if (_is_shown_on_screen()) { + const Size& cnv_size = get_canvas_size(); + _resize((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height()); + + // Because of performance problems on macOS, where PaintEvents are not delivered + // frequently enough, we call render() here directly when we can. + render(); + } +} + +#if ENABLE_RAYCAST_PICKING +void GLCanvas3D::_picking_pass() +{ + if (!m_picking_enabled || m_mouse.dragging || m_mouse.position == Vec2d(DBL_MAX, DBL_MAX) || m_gizmos.is_dragging()) { +#if ENABLE_RAYCAST_PICKING_DEBUG + ImGuiWrapper& imgui = *wxGetApp().imgui(); + imgui.begin(std::string("Hit result"), ImGuiWindowFlags_AlwaysAutoResize); + imgui.text("Picking disabled"); + imgui.end(); +#endif // ENABLE_RAYCAST_PICKING_DEBUG + return; + } + + m_hover_volume_idxs.clear(); + + const ClippingPlane clipping_plane = m_gizmos.get_clipping_plane().inverted_normal(); + const SceneRaycaster::HitResult hit = m_scene_raycaster.hit(m_mouse.position, wxGetApp().plater()->get_camera(), &clipping_plane); + if (hit.is_valid()) { + switch (hit.type) + { + case SceneRaycaster::EType::Volume: + { + if (0 <= hit.raycaster_id && hit.raycaster_id < (int)m_volumes.volumes.size()) { + const GLVolume* volume = m_volumes.volumes[hit.raycaster_id]; + if (volume->is_active && !volume->disabled && (volume->composite_id.volume_id >= 0 || m_render_sla_auxiliaries)) { + // do not add the volume id if any gizmo is active and CTRL is pressed + if (m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined || !wxGetKeyState(WXK_CONTROL)) { + m_hover_volume_idxs.emplace_back(hit.raycaster_id); + m_gizmos.set_hover_id(-1); + } + } + } + else + assert(false); + + break; + } + case SceneRaycaster::EType::Gizmo: + { + const Size& cnv_size = get_canvas_size(); + bool inside = 0 <= m_mouse.position.x() && m_mouse.position.x() < cnv_size.get_width() && + 0 <= m_mouse.position.y() && m_mouse.position.y() < cnv_size.get_height(); + m_gizmos.set_hover_id(inside ? hit.raycaster_id : -1); + break; + } + case SceneRaycaster::EType::Bed: + { + m_gizmos.set_hover_id(-1); + break; + } + default: + { + assert(false); + break; + } + } + } + else + m_gizmos.set_hover_id(-1); + + _update_volumes_hover_state(); + +#if ENABLE_RAYCAST_PICKING_DEBUG + ImGuiWrapper& imgui = *wxGetApp().imgui(); + imgui.begin(std::string("Hit result"), ImGuiWindowFlags_AlwaysAutoResize); + std::string object_type = "None"; + switch (hit.type) + { + case SceneRaycaster::EType::Bed: { object_type = "Bed"; break; } + case SceneRaycaster::EType::Gizmo: { object_type = "Gizmo element"; break; } + case SceneRaycaster::EType::Volume: + { + if (m_volumes.volumes[hit.raycaster_id]->is_wipe_tower) + object_type = "Volume (Wipe tower)"; + else if (m_volumes.volumes[hit.raycaster_id]->volume_idx() == -int(slaposPad)) + object_type = "Volume (SLA pad)"; + else if (m_volumes.volumes[hit.raycaster_id]->volume_idx() == -int(slaposSupportTree)) + object_type = "Volume (SLA supports)"; + else if (m_volumes.volumes[hit.raycaster_id]->is_modifier) + object_type = "Volume (Modifier)"; + else + object_type = "Volume (Part)"; + break; + } + default: { break; } + } + + auto add_strings_row_to_table = [&imgui](const std::string& col_1, const ImVec4& col_1_color, const std::string& col_2, const ImVec4& col_2_color) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + imgui.text_colored(col_1_color, col_1.c_str()); + ImGui::TableSetColumnIndex(1); + imgui.text_colored(col_2_color, col_2.c_str()); + }; + + char buf[1024]; + if (hit.type != SceneRaycaster::EType::None) { + if (ImGui::BeginTable("Hit", 2)) { + add_strings_row_to_table("Object ID", ImGuiWrapper::COL_ORANGE_LIGHT, std::to_string(hit.raycaster_id), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + add_strings_row_to_table("Type", ImGuiWrapper::COL_ORANGE_LIGHT, object_type, ImGui::GetStyleColorVec4(ImGuiCol_Text)); + sprintf(buf, "%.3f, %.3f, %.3f", hit.position.x(), hit.position.y(), hit.position.z()); + add_strings_row_to_table("Position", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + sprintf(buf, "%.3f, %.3f, %.3f", hit.normal.x(), hit.normal.y(), hit.normal.z()); + add_strings_row_to_table("Normal", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + ImGui::EndTable(); + } + } + else + imgui.text("NO HIT"); + + ImGui::Separator(); + imgui.text("Registered for picking:"); + if (ImGui::BeginTable("Raycasters", 2)) { + sprintf(buf, "%d (%d)", (int)m_scene_raycaster.beds_count(), (int)m_scene_raycaster.active_beds_count()); + add_strings_row_to_table("Beds", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + sprintf(buf, "%d (%d)", (int)m_scene_raycaster.volumes_count(), (int)m_scene_raycaster.active_volumes_count()); + add_strings_row_to_table("Volumes", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + sprintf(buf, "%d (%d)", (int)m_scene_raycaster.gizmos_count(), (int)m_scene_raycaster.active_gizmos_count()); + add_strings_row_to_table("Gizmo elements", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + ImGui::EndTable(); + } + imgui.end(); +#endif // ENABLE_RAYCAST_PICKING_DEBUG +} +#else +void GLCanvas3D::_picking_pass() +{ + if (m_picking_enabled && !m_mouse.dragging && m_mouse.position != Vec2d(DBL_MAX, DBL_MAX) && !m_gizmos.is_dragging()) { + m_hover_volume_idxs.clear(); + + // Render the object for picking. + // FIXME This cannot possibly work in a multi - sampled context as the color gets mangled by the anti - aliasing. + // Better to use software ray - casting on a bounding - box hierarchy. + + if (m_multisample_allowed) + // This flag is often ignored by NVIDIA drivers if rendering into a screen buffer. + glsafe(::glDisable(GL_MULTISAMPLE)); + + glsafe(::glDisable(GL_BLEND)); + glsafe(::glEnable(GL_DEPTH_TEST)); + + glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + m_camera_clipping_plane = m_gizmos.get_clipping_plane(); + if (m_camera_clipping_plane.is_active()) { + ::glClipPlane(GL_CLIP_PLANE0, (GLdouble*)m_camera_clipping_plane.get_data().data()); + ::glEnable(GL_CLIP_PLANE0); + } +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + _render_volumes_for_picking(); +#if !ENABLE_LEGACY_OPENGL_REMOVAL + if (m_camera_clipping_plane.is_active()) + ::glDisable(GL_CLIP_PLANE0); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Camera& camera = wxGetApp().plater()->get_camera(); + _render_bed_for_picking(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward()); +#else + _render_bed_for_picking(!wxGetApp().plater()->get_camera().is_looking_downward()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + m_gizmos.render_current_gizmo_for_picking_pass(); + + if (m_multisample_allowed) + glsafe(::glEnable(GL_MULTISAMPLE)); + + int volume_id = -1; + int gizmo_id = -1; + + std::array color = { 0, 0, 0, 0 }; + const Size& cnv_size = get_canvas_size(); + bool inside = 0 <= m_mouse.position(0) && m_mouse.position(0) < cnv_size.get_width() && 0 <= m_mouse.position(1) && m_mouse.position(1) < cnv_size.get_height(); + if (inside) { + glsafe(::glReadPixels(m_mouse.position(0), cnv_size.get_height() - m_mouse.position.y() - 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, (void*)color.data())); + if (picking_checksum_alpha_channel(color[0], color[1], color[2]) == color[3]) { + // Only non-interpolated colors are valid, those have their lowest three bits zeroed. + // we reserve color = (0,0,0) for occluders (as the printbed) + // volumes' id are shifted by 1 + // see: _render_volumes_for_picking() + unsigned int id = picking_encode(color[0], color[1], color[2]); + volume_id = id - 1; + // gizmos' id are instead properly encoded by the color + gizmo_id = id; + } + } + if (0 <= volume_id && volume_id < (int)m_volumes.volumes.size()) { + // do not add the volume id if any gizmo is active and CTRL is pressed + if (m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined || !wxGetKeyState(WXK_CONTROL)) + m_hover_volume_idxs.emplace_back(volume_id); + m_gizmos.set_hover_id(-1); + } + else + m_gizmos.set_hover_id(inside && (unsigned int)gizmo_id <= GLGizmoBase::BASE_ID ? ((int)GLGizmoBase::BASE_ID - gizmo_id) : -1); + + _update_volumes_hover_state(); + } +} +#endif // ENABLE_RAYCAST_PICKING + +void GLCanvas3D::_rectangular_selection_picking_pass() +{ + m_gizmos.set_hover_id(-1); + + std::set idxs; + + if (m_picking_enabled) { +#if ENABLE_RAYCAST_PICKING + const size_t width = std::max(m_rectangle_selection.get_width(), 1); + const size_t height = std::max(m_rectangle_selection.get_height(), 1); + + const OpenGLManager::EFramebufferType framebuffers_type = OpenGLManager::get_framebuffers_type(); + bool use_framebuffer = framebuffers_type != OpenGLManager::EFramebufferType::Unknown; + + GLuint render_fbo = 0; + GLuint render_tex = 0; + GLuint render_depth = 0; + if (use_framebuffer) { + // setup a framebuffer which covers only the selection rectangle + if (framebuffers_type == OpenGLManager::EFramebufferType::Arb) { + glsafe(::glGenFramebuffers(1, &render_fbo)); + glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, render_fbo)); + } + else { + glsafe(::glGenFramebuffersEXT(1, &render_fbo)); + glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, render_fbo)); + } + glsafe(::glGenTextures(1, &render_tex)); + glsafe(::glBindTexture(GL_TEXTURE_2D, render_tex)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)); + if (framebuffers_type == OpenGLManager::EFramebufferType::Arb) { + glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, render_tex, 0)); + glsafe(::glGenRenderbuffers(1, &render_depth)); + glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, render_depth)); +#if ENABLE_OPENGL_ES + glsafe(::glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height)); +#else + glsafe(::glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height)); +#endif // ENABLE_OPENGL_ES + glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, render_depth)); + } + else { + glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, render_tex, 0)); + glsafe(::glGenRenderbuffersEXT(1, &render_depth)); + glsafe(::glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, render_depth)); + glsafe(::glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, width, height)); + glsafe(::glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, render_depth)); + } + const GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 }; + glsafe(::glDrawBuffers(1, drawBufs)); + if (framebuffers_type == OpenGLManager::EFramebufferType::Arb) { + if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + use_framebuffer = false; + } + else { + if (::glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) != GL_FRAMEBUFFER_COMPLETE_EXT) + use_framebuffer = false; + } + } +#endif // ENABLE_RAYCAST_PICKING + + if (m_multisample_allowed) + // This flag is often ignored by NVIDIA drivers if rendering into a screen buffer. + glsafe(::glDisable(GL_MULTISAMPLE)); + + glsafe(::glDisable(GL_BLEND)); + glsafe(::glEnable(GL_DEPTH_TEST)); + + glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); + +#if ENABLE_RAYCAST_PICKING + const Camera& main_camera = wxGetApp().plater()->get_camera(); + Camera framebuffer_camera; + const Camera* camera = &main_camera; + if (use_framebuffer) { + // setup a camera which covers only the selection rectangle + const std::array& viewport = camera->get_viewport(); + const double near_left = camera->get_near_left(); + const double near_bottom = camera->get_near_bottom(); + const double near_width = camera->get_near_width(); + const double near_height = camera->get_near_height(); + + const double ratio_x = near_width / double(viewport[2]); + const double ratio_y = near_height / double(viewport[3]); + + const double rect_near_left = near_left + double(m_rectangle_selection.get_left()) * ratio_x; + const double rect_near_bottom = near_bottom + (double(viewport[3]) - double(m_rectangle_selection.get_bottom())) * ratio_y; + double rect_near_right = near_left + double(m_rectangle_selection.get_right()) * ratio_x; + double rect_near_top = near_bottom + (double(viewport[3]) - double(m_rectangle_selection.get_top())) * ratio_y; + + if (rect_near_left == rect_near_right) + rect_near_right = rect_near_left + ratio_x; + if (rect_near_bottom == rect_near_top) + rect_near_top = rect_near_bottom + ratio_y; + + framebuffer_camera.look_at(camera->get_position(), camera->get_target(), camera->get_dir_up()); + framebuffer_camera.apply_projection(rect_near_left, rect_near_right, rect_near_bottom, rect_near_top, camera->get_near_z(), camera->get_far_z()); + framebuffer_camera.set_viewport(0, 0, width, height); + framebuffer_camera.apply_viewport(); + camera = &framebuffer_camera; + } + + _render_volumes_for_picking(*camera); +#else + _render_volumes_for_picking(); +#endif // ENABLE_RAYCAST_PICKING +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_RAYCAST_PICKING + _render_bed_for_picking(camera->get_view_matrix(), camera->get_projection_matrix(), !camera->is_looking_downward()); +#else + const Camera& camera = wxGetApp().plater()->get_camera(); + _render_bed_for_picking(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward()); +#endif // ENABLE_RAYCAST_PICKING +#else + _render_bed_for_picking(!wxGetApp().plater()->get_camera().is_looking_downward()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + if (m_multisample_allowed) + glsafe(::glEnable(GL_MULTISAMPLE)); + +#if ENABLE_RAYCAST_PICKING + const size_t px_count = width * height; + + const size_t left = use_framebuffer ? 0 : (size_t)m_rectangle_selection.get_left(); + const size_t top = use_framebuffer ? 0 : (size_t)get_canvas_size().get_height() - (size_t)m_rectangle_selection.get_top(); +#else + int width = std::max((int)m_rectangle_selection.get_width(), 1); + int height = std::max((int)m_rectangle_selection.get_height(), 1); + int px_count = width * height; + + int left = (int)m_rectangle_selection.get_left(); + int top = get_canvas_size().get_height() - (int)m_rectangle_selection.get_top(); + if (left >= 0 && top >= 0) { +#endif // ENABLE_RAYCAST_PICKING +#define USE_PARALLEL 1 +#if USE_PARALLEL + struct Pixel + { + std::array data; + // Only non-interpolated colors are valid, those have their lowest three bits zeroed. + bool valid() const { return picking_checksum_alpha_channel(data[0], data[1], data[2]) == data[3]; } + // we reserve color = (0,0,0) for occluders (as the printbed) + // volumes' id are shifted by 1 + // see: _render_volumes_for_picking() + int id() const { return data[0] + (data[1] << 8) + (data[2] << 16) - 1; } + }; + + std::vector frame(px_count); + glsafe(::glReadPixels(left, top, width, height, GL_RGBA, GL_UNSIGNED_BYTE, (void*)frame.data())); + + tbb::spin_mutex mutex; + tbb::parallel_for(tbb::blocked_range(0, frame.size(), (size_t)width), + [this, &frame, &idxs, &mutex](const tbb::blocked_range& range) { + for (size_t i = range.begin(); i < range.end(); ++i) + if (frame[i].valid()) { + int volume_id = frame[i].id(); + if (0 <= volume_id && volume_id < (int)m_volumes.volumes.size()) { + mutex.lock(); + idxs.insert(volume_id); + mutex.unlock(); + } + } + }); +#else + std::vector frame(4 * px_count); + glsafe(::glReadPixels(left, top, width, height, GL_RGBA, GL_UNSIGNED_BYTE, (void*)frame.data())); + + for (int i = 0; i < px_count; ++i) + { + int px_id = 4 * i; + int volume_id = frame[px_id] + (frame[px_id + 1] << 8) + (frame[px_id + 2] << 16); + if (0 <= volume_id && volume_id < (int)m_volumes.volumes.size()) + idxs.insert(volume_id); + } +#endif // USE_PARALLEL +#if ENABLE_RAYCAST_PICKING + if (camera != &main_camera) + main_camera.apply_viewport(); + + if (framebuffers_type == OpenGLManager::EFramebufferType::Arb) { + glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, 0)); + if (render_depth != 0) + glsafe(::glDeleteRenderbuffers(1, &render_depth)); + if (render_fbo != 0) + glsafe(::glDeleteFramebuffers(1, &render_fbo)); + } + else if (framebuffers_type == OpenGLManager::EFramebufferType::Ext) { + glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0)); + if (render_depth != 0) + glsafe(::glDeleteRenderbuffersEXT(1, &render_depth)); + if (render_fbo != 0) + glsafe(::glDeleteFramebuffersEXT(1, &render_fbo)); + } + + if (render_tex != 0) + glsafe(::glDeleteTextures(1, &render_tex)); +#else + } +#endif // ENABLE_RAYCAST_PICKING + } + + m_hover_volume_idxs.assign(idxs.begin(), idxs.end()); + _update_volumes_hover_state(); +} + +void GLCanvas3D::_render_background() +{ + bool use_error_color = false; + if (wxGetApp().is_editor()) { + use_error_color = m_dynamic_background_enabled && + (current_printer_technology() != ptSLA || !m_volumes.empty()); + + if (!m_volumes.empty()) + use_error_color &= _is_any_volume_outside().first; + else + use_error_color &= m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed(); + } + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPushMatrix()); + glsafe(::glLoadIdentity()); + glsafe(::glMatrixMode(GL_PROJECTION)); + glsafe(::glPushMatrix()); + glsafe(::glLoadIdentity()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + // Draws a bottom to top gradient over the complete screen. + glsafe(::glDisable(GL_DEPTH_TEST)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const ColorRGBA bottom_color = use_error_color ? ERROR_BG_DARK_COLOR : DEFAULT_BG_DARK_COLOR; + + if (!m_background.is_initialized()) { + m_background.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P2T2 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(6); + + // vertices + init_data.add_vertex(Vec2f(-1.0f, -1.0f), Vec2f(0.0f, 0.0f)); + init_data.add_vertex(Vec2f(1.0f, -1.0f), Vec2f(1.0f, 0.0f)); + init_data.add_vertex(Vec2f(1.0f, 1.0f), Vec2f(1.0f, 1.0f)); + init_data.add_vertex(Vec2f(-1.0f, 1.0f), Vec2f(0.0f, 1.0f)); + + // indices + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(2, 3, 0); + + m_background.init_from(std::move(init_data)); + } + + GLShaderProgram* shader = wxGetApp().get_shader("background"); + if (shader != nullptr) { + shader->start_using(); + shader->set_uniform("top_color", use_error_color ? ERROR_BG_LIGHT_COLOR : DEFAULT_BG_LIGHT_COLOR); + shader->set_uniform("bottom_color", bottom_color); + m_background.render(); + shader->stop_using(); + } +#else + ::glBegin(GL_QUADS); + ::glColor3fv(use_error_color ? ERROR_BG_DARK_COLOR.data(): DEFAULT_BG_DARK_COLOR.data()); + ::glVertex2f(-1.0f, -1.0f); + ::glVertex2f(1.0f, -1.0f); + + ::glColor3fv(use_error_color ? ERROR_BG_LIGHT_COLOR.data() : DEFAULT_BG_LIGHT_COLOR.data()); + ::glVertex2f(1.0f, 1.0f); + ::glVertex2f(-1.0f, 1.0f); + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glEnable(GL_DEPTH_TEST)); + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); + glsafe(::glMatrixMode(GL_MODELVIEW)); + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_axes) +#else +void GLCanvas3D::_render_bed(bool bottom, bool show_axes) +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ + float scale_factor = 1.0; +#if ENABLE_RETINA_GL + scale_factor = m_retina_helper->get_scale_factor(); +#endif // ENABLE_RETINA_GL + + bool show_texture = ! bottom || + (m_gizmos.get_current_type() != GLGizmosManager::FdmSupports + && m_gizmos.get_current_type() != GLGizmosManager::SlaSupports + && m_gizmos.get_current_type() != GLGizmosManager::Hollow + && m_gizmos.get_current_type() != GLGizmosManager::Seam + && m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes, show_texture); +#else + m_bed.render(*this, bottom, scale_factor, show_axes, show_texture); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLCanvas3D::_render_bed_for_picking(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom) +#else +void GLCanvas3D::_render_bed_for_picking(bool bottom) +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ + float scale_factor = 1.0; +#if ENABLE_RETINA_GL + scale_factor = m_retina_helper->get_scale_factor(); +#endif // ENABLE_RETINA_GL + +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_bed.render_for_picking(*this, view_matrix, projection_matrix, bottom, scale_factor); +#else + m_bed.render_for_picking(*this, bottom, scale_factor); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type) +{ + if (m_volumes.empty()) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + + m_camera_clipping_plane = m_gizmos.get_clipping_plane(); + + if (m_picking_enabled) + // Update the layer editing selection to the first object selected, update the current object maximum Z. + m_layers_editing.select_object(*m_model, this->is_layers_editing_enabled() ? m_selection.get_object_idx() : -1); + + if (const BuildVolume &build_volume = m_bed.build_volume(); build_volume.valid()) { + switch (build_volume.type()) { + case BuildVolume::Type::Rectangle: { + const BoundingBox3Base bed_bb = build_volume.bounding_volume().inflated(BuildVolume::SceneEpsilon); + m_volumes.set_print_volume({ 0, // circle + { float(bed_bb.min.x()), float(bed_bb.min.y()), float(bed_bb.max.x()), float(bed_bb.max.y()) }, + { 0.0f, float(build_volume.max_print_height()) } }); + break; + } + case BuildVolume::Type::Circle: { + m_volumes.set_print_volume({ 1, // rectangle + { unscaled(build_volume.circle().center.x()), unscaled(build_volume.circle().center.y()), unscaled(build_volume.circle().radius + BuildVolume::SceneEpsilon), 0.0f }, + { 0.0f, float(build_volume.max_print_height() + BuildVolume::SceneEpsilon) } }); + break; + } + default: + case BuildVolume::Type::Convex: + case BuildVolume::Type::Custom: { + m_volumes.set_print_volume({ static_cast(type), + { -FLT_MAX, -FLT_MAX, FLT_MAX, FLT_MAX }, + { -FLT_MAX, FLT_MAX } } + ); + } + } + if (m_requires_check_outside_state) { + m_volumes.check_outside_state(build_volume, nullptr); + m_requires_check_outside_state = false; + } + } + + if (m_use_clipping_planes) + m_volumes.set_z_range(-m_clipping_planes[0].get_data()[3], m_clipping_planes[1].get_data()[3]); + else + m_volumes.set_z_range(-FLT_MAX, FLT_MAX); + + m_volumes.set_clipping_plane(m_camera_clipping_plane.get_data()); + m_volumes.set_show_sinking_contours(! m_gizmos.is_hiding_instances()); + m_volumes.set_show_non_manifold_edges(!m_gizmos.is_hiding_instances() && m_gizmos.get_current_type() != GLGizmosManager::Simplify); + + GLShaderProgram* shader = wxGetApp().get_shader("gouraud"); + if (shader != nullptr) { + shader->start_using(); + + switch (type) + { + default: + case GLVolumeCollection::ERenderType::Opaque: + { + if (m_picking_enabled && !m_gizmos.is_dragging() && m_layers_editing.is_enabled() && (m_layers_editing.last_object_id != -1) && (m_layers_editing.object_max_z() > 0.0f)) { + int object_id = m_layers_editing.last_object_id; +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Camera& camera = wxGetApp().plater()->get_camera(); + m_volumes.render(type, false, camera.get_view_matrix(), camera.get_projection_matrix(), [object_id](const GLVolume& volume) { + // Which volume to paint without the layer height profile shader? + return volume.is_active && (volume.is_modifier || volume.composite_id.object_id != object_id); + }); +#else + m_volumes.render(type, false, wxGetApp().plater()->get_camera().get_view_matrix(), [object_id](const GLVolume& volume) { + // Which volume to paint without the layer height profile shader? + return volume.is_active && (volume.is_modifier || volume.composite_id.object_id != object_id); + }); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + // Let LayersEditing handle rendering of the active object using the layer height profile shader. + m_layers_editing.render_volumes(*this, m_volumes); + } + else { + // do not cull backfaces to show broken geometry, if any +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Camera& camera = wxGetApp().plater()->get_camera(); + m_volumes.render(type, m_picking_enabled, camera.get_view_matrix(), camera.get_projection_matrix(), [this](const GLVolume& volume) { + return (m_render_sla_auxiliaries || volume.composite_id.volume_id >= 0); + }); +#else + m_volumes.render(type, m_picking_enabled, wxGetApp().plater()->get_camera().get_view_matrix(), [this](const GLVolume& volume) { + return (m_render_sla_auxiliaries || volume.composite_id.volume_id >= 0); + }); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + + // In case a painting gizmo is open, it should render the painted triangles + // before transparent objects are rendered. Otherwise they would not be + // visible when inside modifier meshes etc. + { + GLGizmosManager& gm = get_gizmos_manager(); +// GLGizmosManager::EType type = gm.get_current_type(); + if (dynamic_cast(gm.get_current())) { + shader->stop_using(); + gm.render_painter_gizmo(); + shader->start_using(); + } + } + break; + } + case GLVolumeCollection::ERenderType::Transparent: + { +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Camera& camera = wxGetApp().plater()->get_camera(); + m_volumes.render(type, false, camera.get_view_matrix(), camera.get_projection_matrix()); +#else + m_volumes.render(type, false, wxGetApp().plater()->get_camera().get_view_matrix()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + break; + } + } + shader->stop_using(); + } + + m_camera_clipping_plane = ClippingPlane::ClipsNothing(); +} + +void GLCanvas3D::_render_gcode() +{ + m_gcode_viewer.render(); +} + +void GLCanvas3D::_render_gcode_cog() +{ + m_gcode_viewer.render_cog(); +} + +void GLCanvas3D::_render_selection() +{ + float scale_factor = 1.0; +#if ENABLE_RETINA_GL + scale_factor = m_retina_helper->get_scale_factor(); +#endif // ENABLE_RETINA_GL + + if (!m_gizmos.is_running()) + m_selection.render(scale_factor); +} + +void GLCanvas3D::_render_sequential_clearance() +{ + if (m_layers_editing.is_enabled() || m_gizmos.is_dragging()) + return; + + switch (m_gizmos.get_current_type()) + { + case GLGizmosManager::EType::Flatten: + case GLGizmosManager::EType::Cut: + case GLGizmosManager::EType::Hollow: + case GLGizmosManager::EType::SlaSupports: + case GLGizmosManager::EType::FdmSupports: + case GLGizmosManager::EType::Seam: { return; } + default: { break; } + } + + m_sequential_print_clearance.render(); +} + +#if ENABLE_RENDER_SELECTION_CENTER +void GLCanvas3D::_render_selection_center() +{ + m_selection.render_center(m_gizmos.is_dragging()); +} +#endif // ENABLE_RENDER_SELECTION_CENTER + +void GLCanvas3D::_check_and_update_toolbar_icon_scale() +{ + // Don't update a toolbar scale, when we are on a Preview + if (wxGetApp().plater()->is_preview_shown()) + return; + + float scale = wxGetApp().toolbar_icon_scale(); + Size cnv_size = get_canvas_size(); + + float size = GLToolbar::Default_Icons_Size * scale; + + // Set current size for all top toolbars. It will be used for next calculations + GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar(); +#if ENABLE_RETINA_GL + const float sc = m_retina_helper->get_scale_factor() * scale; + m_main_toolbar.set_scale(sc); + m_undoredo_toolbar.set_scale(sc); + collapse_toolbar.set_scale(sc); + size *= m_retina_helper->get_scale_factor(); +#else + m_main_toolbar.set_icons_size(size); + m_undoredo_toolbar.set_icons_size(size); + collapse_toolbar.set_icons_size(size); +#endif // ENABLE_RETINA_GL + + float top_tb_width = m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar.get_width(); + int items_cnt = m_main_toolbar.get_visible_items_cnt() + m_undoredo_toolbar.get_visible_items_cnt() + collapse_toolbar.get_visible_items_cnt(); + float noitems_width = top_tb_width - size * items_cnt; // width of separators and borders in top toolbars + + // calculate scale needed for items in all top toolbars + // the std::max() is there because on some Linux dialects/virtual machines this code is called when the canvas has not been properly initialized yet, + // leading to negative values for the scale. + // See: https://github.com/prusa3d/PrusaSlicer/issues/8563 + // https://github.com/supermerill/SuperSlicer/issues/854 + float new_h_scale = std::max((cnv_size.get_width() - noitems_width), 1.0f) / (items_cnt * GLToolbar::Default_Icons_Size); + + items_cnt = m_gizmos.get_selectable_icons_cnt() + 3; // +3 means a place for top and view toolbars and separators in gizmos toolbar + + // calculate scale needed for items in the gizmos toolbar + float new_v_scale = cnv_size.get_height() / (items_cnt * GLGizmosManager::Default_Icons_Size); + + // set minimum scale as a auto scale for the toolbars + float new_scale = std::min(new_h_scale, new_v_scale); +#if ENABLE_RETINA_GL + new_scale /= m_retina_helper->get_scale_factor(); +#endif + if (fabs(new_scale - scale) > 0.01) // scale is changed by 1% and more + wxGetApp().set_auto_toolbar_icon_scale(new_scale); +} + +void GLCanvas3D::_render_overlays() +{ + glsafe(::glDisable(GL_DEPTH_TEST)); +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPushMatrix()); + glsafe(::glLoadIdentity()); + // ensure that the textures are renderered inside the frustrum + const Camera& camera = wxGetApp().plater()->get_camera(); + glsafe(::glTranslated(0.0, 0.0, -(camera.get_near_z() + 0.005))); + // ensure that the overlay fits the frustrum near z plane + double gui_scale = camera.get_gui_scale(); + glsafe(::glScaled(gui_scale, gui_scale, 1.0)); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + _check_and_update_toolbar_icon_scale(); + + _render_gizmos_overlay(); + + // main toolbar and undoredo toolbar need to be both updated before rendering because both their sizes are needed + // to correctly place them +#if ENABLE_RETINA_GL + const float scale = m_retina_helper->get_scale_factor() * wxGetApp().toolbar_icon_scale(/*true*/); + m_main_toolbar.set_scale(scale); + m_undoredo_toolbar.set_scale(scale); + wxGetApp().plater()->get_collapse_toolbar().set_scale(scale); +#else + const float size = int(GLToolbar::Default_Icons_Size * wxGetApp().toolbar_icon_scale(/*true*/)); + m_main_toolbar.set_icons_size(size); + m_undoredo_toolbar.set_icons_size(size); + wxGetApp().plater()->get_collapse_toolbar().set_icons_size(size); +#endif // ENABLE_RETINA_GL + + _render_main_toolbar(); + _render_undoredo_toolbar(); + _render_collapse_toolbar(); + _render_view_toolbar(); + + if (m_layers_editing.last_object_id >= 0 && m_layers_editing.object_max_z() > 0.0f) + m_layers_editing.render_overlay(*this); + + const ConfigOptionBool* opt = dynamic_cast(m_config->option("complete_objects")); + bool sequential_print = opt != nullptr && opt->value; + std::vector sorted_instances; + if (sequential_print) { + for (ModelObject* model_object : m_model->objects) + for (ModelInstance* model_instance : model_object->instances) { + sorted_instances.emplace_back(model_instance); + } + } + m_labels.render(sorted_instances); + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_RAYCAST_PICKING +void GLCanvas3D::_render_volumes_for_picking(const Camera& camera) const +#else +void GLCanvas3D::_render_volumes_for_picking() const +#endif // ENABLE_RAYCAST_PICKING +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader("flat_clip"); + if (shader == nullptr) + return; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + // do not cull backfaces to show broken geometry, if any + glsafe(::glDisable(GL_CULL_FACE)); + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glEnableClientState(GL_VERTEX_ARRAY)); + glsafe(::glEnableClientState(GL_NORMAL_ARRAY)); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_RAYCAST_PICKING + const Transform3d& view_matrix = camera.get_view_matrix(); +#else + const Transform3d& view_matrix = wxGetApp().plater()->get_camera().get_view_matrix(); +#endif // ENABLE_RAYCAST_PICKING + for (size_t type = 0; type < 2; ++ type) { + GLVolumeWithIdAndZList to_render = volumes_to_render(m_volumes.volumes, (type == 0) ? GLVolumeCollection::ERenderType::Opaque : GLVolumeCollection::ERenderType::Transparent, view_matrix); + for (const GLVolumeWithIdAndZ& volume : to_render) + if (!volume.first->disabled && (volume.first->composite_id.volume_id >= 0 || m_render_sla_auxiliaries)) { + // Object picking mode. Render the object with a color encoding the object index. + // we reserve color = (0,0,0) for occluders (as the printbed) + // so we shift volumes' id by 1 to get the proper color + const unsigned int id = 1 + volume.second.first; +#if ENABLE_LEGACY_OPENGL_REMOVAL + volume.first->model.set_color(picking_decode(id)); + shader->start_using(); +#if ENABLE_RAYCAST_PICKING + shader->set_uniform("view_model_matrix", view_matrix * volume.first->world_matrix()); +#else + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * volume.first->world_matrix()); +#endif // ENABLE_RAYCAST_PICKING + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + shader->set_uniform("volume_world_matrix", volume.first->world_matrix()); + shader->set_uniform("z_range", m_volumes.get_z_range()); + shader->set_uniform("clipping_plane", m_volumes.get_clipping_plane()); +#else + glsafe(::glColor4fv(picking_decode(id).data())); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + volume.first->render(); +#if ENABLE_LEGACY_OPENGL_REMOVAL + shader->stop_using(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + } + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glDisableClientState(GL_NORMAL_ARRAY)); + glsafe(::glDisableClientState(GL_VERTEX_ARRAY)); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glEnable(GL_CULL_FACE)); +} + +void GLCanvas3D::_render_current_gizmo() const +{ + m_gizmos.render_current_gizmo(); +} + +void GLCanvas3D::_render_gizmos_overlay() +{ +#if ENABLE_RETINA_GL +// m_gizmos.set_overlay_scale(m_retina_helper->get_scale_factor()); + const float scale = m_retina_helper->get_scale_factor()*wxGetApp().toolbar_icon_scale(); + m_gizmos.set_overlay_scale(scale); //! #ys_FIXME_experiment +#else +// m_gizmos.set_overlay_scale(m_canvas->GetContentScaleFactor()); +// m_gizmos.set_overlay_scale(wxGetApp().em_unit()*0.1f); + const float size = int(GLGizmosManager::Default_Icons_Size * wxGetApp().toolbar_icon_scale()); + m_gizmos.set_overlay_icon_size(size); //! #ys_FIXME_experiment +#endif /* __WXMSW__ */ + + m_gizmos.render_overlay(); + + if (m_gizmo_highlighter.m_render_arrow) + m_gizmos.render_arrow(*this, m_gizmo_highlighter.m_gizmo_type); +} + +void GLCanvas3D::_render_main_toolbar() +{ + if (!m_main_toolbar.is_enabled()) + return; + + const Size cnv_size = get_canvas_size(); +#if ENABLE_LEGACY_OPENGL_REMOVAL + const float top = 0.5f * (float)cnv_size.get_height(); +#else + const float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom(); + const float top = 0.5f * (float)cnv_size.get_height() * inv_zoom; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar(); + const float collapse_toolbar_width = collapse_toolbar.is_enabled() ? collapse_toolbar.get_width() : 0.0f; +#if ENABLE_LEGACY_OPENGL_REMOVAL + const float left = -0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width); +#else + const float left = -0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width) * inv_zoom; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + m_main_toolbar.set_position(top, left); + m_main_toolbar.render(*this); + if (m_toolbar_highlighter.m_render_arrow) + m_main_toolbar.render_arrow(*this, m_toolbar_highlighter.m_toolbar_item); +} + +void GLCanvas3D::_render_undoredo_toolbar() +{ + if (!m_undoredo_toolbar.is_enabled()) + return; + + const Size cnv_size = get_canvas_size(); +#if ENABLE_LEGACY_OPENGL_REMOVAL + const float top = 0.5f * (float)cnv_size.get_height(); +#else + float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom(); + + const float top = 0.5f * (float)cnv_size.get_height() * inv_zoom; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar(); + const float collapse_toolbar_width = collapse_toolbar.is_enabled() ? collapse_toolbar.get_width() : 0.0f; +#if ENABLE_LEGACY_OPENGL_REMOVAL + const float left = m_main_toolbar.get_width() - 0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width); +#else + const float left = (m_main_toolbar.get_width() - 0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width)) * inv_zoom; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + m_undoredo_toolbar.set_position(top, left); + m_undoredo_toolbar.render(*this); + if (m_toolbar_highlighter.m_render_arrow) + m_undoredo_toolbar.render_arrow(*this, m_toolbar_highlighter.m_toolbar_item); +} + +void GLCanvas3D::_render_collapse_toolbar() const +{ + GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar(); + + const Size cnv_size = get_canvas_size(); + const float band = m_layers_editing.is_enabled() ? (wxGetApp().imgui()->get_style_scaling() * LayersEditing::THICKNESS_BAR_WIDTH) : 0.0; +#if ENABLE_LEGACY_OPENGL_REMOVAL + const float top = 0.5f * (float)cnv_size.get_height(); + const float left = 0.5f * (float)cnv_size.get_width() - collapse_toolbar.get_width() - band; +#else + const float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom(); + + const float top = 0.5f * (float)cnv_size.get_height() * inv_zoom; + const float left = (0.5f * (float)cnv_size.get_width() - (float)collapse_toolbar.get_width() - band) * inv_zoom; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + collapse_toolbar.set_position(top, left); + collapse_toolbar.render(*this); +} + +void GLCanvas3D::_render_view_toolbar() const +{ + GLToolbar& view_toolbar = wxGetApp().plater()->get_view_toolbar(); + +#if ENABLE_RETINA_GL + const float scale = m_retina_helper->get_scale_factor() * wxGetApp().toolbar_icon_scale(); +#if __APPLE__ + view_toolbar.set_scale(scale); +#else // if GTK3 + const float size = int(GLGizmosManager::Default_Icons_Size * scale); + view_toolbar.set_icons_size(size); +#endif // __APPLE__ +#else + const float size = int(GLGizmosManager::Default_Icons_Size * wxGetApp().toolbar_icon_scale()); + view_toolbar.set_icons_size(size); +#endif // ENABLE_RETINA_GL + + const Size cnv_size = get_canvas_size(); +#if ENABLE_LEGACY_OPENGL_REMOVAL + // places the toolbar on the bottom-left corner of the 3d scene + const float top = -0.5f * (float)cnv_size.get_height() + view_toolbar.get_height(); + const float left = -0.5f * (float)cnv_size.get_width(); +#else + float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom(); + + // places the toolbar on the bottom-left corner of the 3d scene + float top = (-0.5f * (float)cnv_size.get_height() + view_toolbar.get_height()) * inv_zoom; + float left = -0.5f * (float)cnv_size.get_width() * inv_zoom; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + view_toolbar.set_position(top, left); + view_toolbar.render(*this); +} + +#if ENABLE_SHOW_CAMERA_TARGET +void GLCanvas3D::_render_camera_target() +{ + static const float half_length = 5.0f; + + glsafe(::glDisable(GL_DEPTH_TEST)); +#if ENABLE_GL_CORE_PROFILE + if (!OpenGLManager::get_gl_info().is_core_profile()) +#endif // ENABLE_GL_CORE_PROFILE + glsafe(::glLineWidth(2.0f)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Vec3f& target = wxGetApp().plater()->get_camera().get_target().cast(); + m_camera_target.target = target.cast(); + + for (int i = 0; i < 3; ++i) { + if (!m_camera_target.axis[i].is_initialized()) { + m_camera_target.axis[i].reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.color = (i == X) ? ColorRGBA::X() : ((i == Y) ? ColorRGBA::Y() : ColorRGBA::Z()); + init_data.reserve_vertices(2); + init_data.reserve_indices(2); + + // vertices + if (i == X) { + init_data.add_vertex(Vec3f(-half_length, 0.0f, 0.0f)); + init_data.add_vertex(Vec3f(+half_length, 0.0f, 0.0f)); + } + else if (i == Y) { + init_data.add_vertex(Vec3f(0.0f, -half_length, 0.0f)); + init_data.add_vertex(Vec3f(0.0f, +half_length, 0.0f)); + } + else { + init_data.add_vertex(Vec3f(0.0f, 0.0f, -half_length)); + init_data.add_vertex(Vec3f(0.0f, 0.0f, +half_length)); + } + + // indices + init_data.add_line(0, 1); + + m_camera_target.axis[i].init_from(std::move(init_data)); + } + } + +#if ENABLE_GL_CORE_PROFILE + GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); +#else + GLShaderProgram* shader = wxGetApp().get_shader("flat"); +#endif // ENABLE_GL_CORE_PROFILE + if (shader != nullptr) { + shader->start_using(); +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * Geometry::assemble_transform(m_camera_target.target)); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#if ENABLE_GL_CORE_PROFILE + const std::array& viewport = camera.get_viewport(); + shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); + shader->set_uniform("width", 0.5f); + shader->set_uniform("gap_size", 0.0f); +#endif // ENABLE_GL_CORE_PROFILE +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + for (int i = 0; i < 3; ++i) { + m_camera_target.axis[i].render(); + } + shader->stop_using(); + } +#else + ::glBegin(GL_LINES); + const Vec3d& target = wxGetApp().plater()->get_camera().get_target(); + // draw line for x axis + ::glColor3f(1.0f, 0.0f, 0.0f); + ::glVertex3d(target.x() - half_length, target.y(), target.z()); + ::glVertex3d(target.x() + half_length, target.y(), target.z()); + // draw line for y axis + ::glColor3f(0.0f, 1.0f, 0.0f); + ::glVertex3d(target.x(), target.y() - half_length, target.z()); + ::glVertex3d(target.x(), target.y() + half_length, target.z()); + // draw line for z axis + ::glColor3f(0.0f, 0.0f, 1.0f); + ::glVertex3d(target.x(), target.y(), target.z() - half_length); + ::glVertex3d(target.x(), target.y(), target.z() + half_length); + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} +#endif // ENABLE_SHOW_CAMERA_TARGET + +void GLCanvas3D::_render_sla_slices() +{ + if (!m_use_clipping_planes || current_printer_technology() != ptSLA) + return; + + const SLAPrint* print = this->sla_print(); + const PrintObjects& print_objects = print->objects(); + if (print_objects.empty()) + // nothing to render, return + return; + + double clip_min_z = -m_clipping_planes[0].get_data()[3]; + double clip_max_z = m_clipping_planes[1].get_data()[3]; + for (unsigned int i = 0; i < (unsigned int)print_objects.size(); ++i) { + const SLAPrintObject* obj = print_objects[i]; + + if (!obj->is_step_done(slaposSliceSupports)) + continue; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + SlaCap::ObjectIdToModelsMap::iterator it_caps_bottom = m_sla_caps[0].triangles.find(i); + SlaCap::ObjectIdToModelsMap::iterator it_caps_top = m_sla_caps[1].triangles.find(i); +#else + SlaCap::ObjectIdToTrianglesMap::iterator it_caps_bottom = m_sla_caps[0].triangles.find(i); + SlaCap::ObjectIdToTrianglesMap::iterator it_caps_top = m_sla_caps[1].triangles.find(i); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + { + if (it_caps_bottom == m_sla_caps[0].triangles.end()) + it_caps_bottom = m_sla_caps[0].triangles.emplace(i, SlaCap::Triangles()).first; + if (!m_sla_caps[0].matches(clip_min_z)) { + m_sla_caps[0].z = clip_min_z; +#if ENABLE_LEGACY_OPENGL_REMOVAL + it_caps_bottom->second.object.reset(); + it_caps_bottom->second.supports.reset(); +#else + it_caps_bottom->second.object.clear(); + it_caps_bottom->second.supports.clear(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + if (it_caps_top == m_sla_caps[1].triangles.end()) + it_caps_top = m_sla_caps[1].triangles.emplace(i, SlaCap::Triangles()).first; + if (!m_sla_caps[1].matches(clip_max_z)) { + m_sla_caps[1].z = clip_max_z; +#if ENABLE_LEGACY_OPENGL_REMOVAL + it_caps_top->second.object.reset(); + it_caps_top->second.supports.reset(); +#else + it_caps_top->second.object.clear(); + it_caps_top->second.supports.clear(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + } +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLModel& bottom_obj_triangles = it_caps_bottom->second.object; + GLModel& bottom_sup_triangles = it_caps_bottom->second.supports; + GLModel& top_obj_triangles = it_caps_top->second.object; + GLModel& top_sup_triangles = it_caps_top->second.supports; +#else + Pointf3s &bottom_obj_triangles = it_caps_bottom->second.object; + Pointf3s &bottom_sup_triangles = it_caps_bottom->second.supports; + Pointf3s &top_obj_triangles = it_caps_top->second.object; + Pointf3s &top_sup_triangles = it_caps_top->second.supports; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_LEGACY_OPENGL_REMOVAL + auto init_model = [](GLModel& model, const Pointf3s& triangles, const ColorRGBA& color) { + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(triangles.size()); + init_data.reserve_indices(triangles.size() / 3); + init_data.color = color; + + unsigned int vertices_count = 0; + for (const Vec3d& v : triangles) { + init_data.add_vertex((Vec3f)v.cast()); + ++vertices_count; + if (vertices_count % 3 == 0) + init_data.add_triangle(vertices_count - 3, vertices_count - 2, vertices_count - 1); + } + + if (!init_data.is_empty()) + model.init_from(std::move(init_data)); + }; + + if ((!bottom_obj_triangles.is_initialized() || !bottom_sup_triangles.is_initialized() || + !top_obj_triangles.is_initialized() || !top_sup_triangles.is_initialized()) && !obj->get_slice_index().empty()) { +#else + if ((bottom_obj_triangles.empty() || bottom_sup_triangles.empty() || top_obj_triangles.empty() || top_sup_triangles.empty()) && + !obj->get_slice_index().empty()) { +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + double layer_height = print->default_object_config().layer_height.value; + double initial_layer_height = print->material_config().initial_layer_height.value; + bool left_handed = obj->is_left_handed(); + + coord_t key_zero = obj->get_slice_index().front().print_level(); + // Slice at the center of the slab starting at clip_min_z will be rendered for the lower plane. + coord_t key_low = coord_t((clip_min_z - initial_layer_height + layer_height) / SCALING_FACTOR) + key_zero; + // Slice at the center of the slab ending at clip_max_z will be rendered for the upper plane. + coord_t key_high = coord_t((clip_max_z - initial_layer_height) / SCALING_FACTOR) + key_zero; + + const SliceRecord& slice_low = obj->closest_slice_to_print_level(key_low, coord_t(SCALED_EPSILON)); + const SliceRecord& slice_high = obj->closest_slice_to_print_level(key_high, coord_t(SCALED_EPSILON)); + + // Offset to avoid OpenGL Z fighting between the object's horizontal surfaces and the triangluated surfaces of the cuts. + const double plane_shift_z = 0.002; + + if (slice_low.is_valid()) { + const ExPolygons& obj_bottom = slice_low.get_slice(soModel); + const ExPolygons& sup_bottom = slice_low.get_slice(soSupport); +#if ENABLE_LEGACY_OPENGL_REMOVAL + // calculate model bottom cap + if (!bottom_obj_triangles.is_initialized() && !obj_bottom.empty()) + init_model(bottom_obj_triangles, triangulate_expolygons_3d(obj_bottom, clip_min_z - plane_shift_z, !left_handed), { 1.0f, 0.37f, 0.0f, 1.0f }); + // calculate support bottom cap + if (!bottom_sup_triangles.is_initialized() && !sup_bottom.empty()) + init_model(bottom_sup_triangles, triangulate_expolygons_3d(sup_bottom, clip_min_z - plane_shift_z, !left_handed), { 1.0f, 0.0f, 0.37f, 1.0f }); +#else + // calculate model bottom cap + if (bottom_obj_triangles.empty() && !obj_bottom.empty()) + bottom_obj_triangles = triangulate_expolygons_3d(obj_bottom, clip_min_z - plane_shift_z, ! left_handed); + // calculate support bottom cap + if (bottom_sup_triangles.empty() && !sup_bottom.empty()) + bottom_sup_triangles = triangulate_expolygons_3d(sup_bottom, clip_min_z - plane_shift_z, !left_handed); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + + if (slice_high.is_valid()) { + const ExPolygons& obj_top = slice_high.get_slice(soModel); + const ExPolygons& sup_top = slice_high.get_slice(soSupport); +#if ENABLE_LEGACY_OPENGL_REMOVAL + // calculate model top cap + if (!top_obj_triangles.is_initialized() && !obj_top.empty()) + init_model(top_obj_triangles, triangulate_expolygons_3d(obj_top, clip_max_z + plane_shift_z, left_handed), { 1.0f, 0.37f, 0.0f, 1.0f }); + // calculate support top cap + if (!top_sup_triangles.is_initialized() && !sup_top.empty()) + init_model(top_sup_triangles, triangulate_expolygons_3d(sup_top, clip_max_z + plane_shift_z, left_handed), { 1.0f, 0.0f, 0.37f, 1.0f }); +#else + // calculate model top cap + if (top_obj_triangles.empty() && !obj_top.empty()) + top_obj_triangles = triangulate_expolygons_3d(obj_top, clip_max_z + plane_shift_z, left_handed); + // calculate support top cap + if (top_sup_triangles.empty() && !sup_top.empty()) + top_sup_triangles = triangulate_expolygons_3d(sup_top, clip_max_z + plane_shift_z, left_handed); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader != nullptr) { + shader->start_using(); + + for (const SLAPrintObject::Instance& inst : obj->instances()) { + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d view_model_matrix = camera.get_view_matrix() * + Geometry::assemble_transform(Vec3d(unscale(inst.shift.x()), unscale(inst.shift.y()), 0.0), + inst.rotation * Vec3d::UnitZ(), Vec3d::Ones(), + obj->is_left_handed() ? /* The polygons are mirrored by X */ Vec3d(-1.0f, 1.0f, 1.0f) : Vec3d::Ones()); + + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + bottom_obj_triangles.render(); + top_obj_triangles.render(); + bottom_sup_triangles.render(); + top_sup_triangles.render(); + } + + shader->stop_using(); + } +#else + if (!bottom_obj_triangles.empty() || !top_obj_triangles.empty() || !bottom_sup_triangles.empty() || !top_sup_triangles.empty()) { + for (const SLAPrintObject::Instance& inst : obj->instances()) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(unscale(inst.shift.x()), unscale(inst.shift.y()), 0.0)); + glsafe(::glRotatef(Geometry::rad2deg(inst.rotation), 0.0f, 0.0f, 1.0f)); + if (obj->is_left_handed()) + // The polygons are mirrored by X. + glsafe(::glScalef(-1.0f, 1.0f, 1.0f)); + glsafe(::glEnableClientState(GL_VERTEX_ARRAY)); + glsafe(::glColor3f(1.0f, 0.37f, 0.0f)); + if (!bottom_obj_triangles.empty()) { + glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)bottom_obj_triangles.front().data())); + glsafe(::glDrawArrays(GL_TRIANGLES, 0, bottom_obj_triangles.size())); + } + if (! top_obj_triangles.empty()) { + glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)top_obj_triangles.front().data())); + glsafe(::glDrawArrays(GL_TRIANGLES, 0, top_obj_triangles.size())); + } + glsafe(::glColor3f(1.0f, 0.0f, 0.37f)); + if (! bottom_sup_triangles.empty()) { + glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)bottom_sup_triangles.front().data())); + glsafe(::glDrawArrays(GL_TRIANGLES, 0, bottom_sup_triangles.size())); + } + if (! top_sup_triangles.empty()) { + glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)top_sup_triangles.front().data())); + glsafe(::glDrawArrays(GL_TRIANGLES, 0, top_sup_triangles.size())); + } + glsafe(::glDisableClientState(GL_VERTEX_ARRAY)); + glsafe(::glPopMatrix()); + } + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } +} + +void GLCanvas3D::_render_selection_sidebar_hints() +{ + m_selection.render_sidebar_hints(m_sidebar_field); +} + +void GLCanvas3D::_update_volumes_hover_state() +{ + for (GLVolume* v : m_volumes.volumes) { + v->hover = GLVolume::HS_None; + } + + if (m_hover_volume_idxs.empty()) + return; + + bool ctrl_pressed = wxGetKeyState(WXK_CONTROL); // additive select/deselect + bool shift_pressed = wxGetKeyState(WXK_SHIFT); // select by rectangle + bool alt_pressed = wxGetKeyState(WXK_ALT); // deselect by rectangle + + if (alt_pressed && (shift_pressed || ctrl_pressed)) { + // illegal combinations of keys + m_hover_volume_idxs.clear(); + return; + } + + bool hover_modifiers_only = true; + for (int i : m_hover_volume_idxs) { + if (!m_volumes.volumes[i]->is_modifier) { + hover_modifiers_only = false; + break; + } + } + + std::set> hover_instances; + for (int i : m_hover_volume_idxs) { + const GLVolume& v = *m_volumes.volumes[i]; + hover_instances.insert(std::make_pair(v.object_idx(), v.instance_idx())); + } + + bool hover_from_single_instance = hover_instances.size() == 1; + + if (hover_modifiers_only && !hover_from_single_instance) { + // do not allow to select volumes from different instances + m_hover_volume_idxs.clear(); + return; + } + + for (int i : m_hover_volume_idxs) { + GLVolume& volume = *m_volumes.volumes[i]; + if (volume.hover != GLVolume::HS_None) + continue; + + bool deselect = volume.selected && ((shift_pressed && m_rectangle_selection.is_empty()) || (alt_pressed && !m_rectangle_selection.is_empty())); + bool select = !volume.selected && (m_rectangle_selection.is_empty() || (shift_pressed && !m_rectangle_selection.is_empty())); + + if (select || deselect) { + bool as_volume = + volume.is_modifier && hover_from_single_instance && !ctrl_pressed && + ( + (!deselect) || + (deselect && !m_selection.is_single_full_instance() && (volume.object_idx() == m_selection.get_object_idx()) && (volume.instance_idx() == m_selection.get_instance_idx())) + ); + + if (as_volume) + volume.hover = deselect ? GLVolume::HS_Deselect : GLVolume::HS_Select; + else { + int object_idx = volume.object_idx(); + int instance_idx = volume.instance_idx(); + + for (GLVolume* v : m_volumes.volumes) { + if (v->object_idx() == object_idx && v->instance_idx() == instance_idx) + v->hover = deselect ? GLVolume::HS_Deselect : GLVolume::HS_Select; + } + } + } + else if (volume.selected) + volume.hover = GLVolume::HS_Hover; + } +} + +void GLCanvas3D::_perform_layer_editing_action(wxMouseEvent* evt) +{ + int object_idx_selected = m_layers_editing.last_object_id; + if (object_idx_selected == -1) + return; + + // A volume is selected. Test, whether hovering over a layer thickness bar. + if (evt != nullptr) { + const Rect& rect = LayersEditing::get_bar_rect_screen(*this); + float b = rect.get_bottom(); + m_layers_editing.last_z = m_layers_editing.object_max_z() * (b - evt->GetY() - 1.0f) / (b - rect.get_top()); + m_layers_editing.last_action = + evt->ShiftDown() ? (evt->RightIsDown() ? LAYER_HEIGHT_EDIT_ACTION_SMOOTH : LAYER_HEIGHT_EDIT_ACTION_REDUCE) : + (evt->RightIsDown() ? LAYER_HEIGHT_EDIT_ACTION_INCREASE : LAYER_HEIGHT_EDIT_ACTION_DECREASE); + } + + m_layers_editing.adjust_layer_height_profile(); + _refresh_if_shown_on_screen(); + + // Automatic action on mouse down with the same coordinate. + _start_timer(); +} + +Vec3d GLCanvas3D::_mouse_to_3d(const Point& mouse_pos, float* z) +{ + if (m_canvas == nullptr) + return Vec3d(DBL_MAX, DBL_MAX, DBL_MAX); + +#if ENABLE_RAYCAST_PICKING + if (z == nullptr) { + const SceneRaycaster::HitResult hit = m_scene_raycaster.hit(mouse_pos.cast(), wxGetApp().plater()->get_camera(), nullptr); + return hit.is_valid() ? hit.position.cast() : _mouse_to_bed_3d(mouse_pos); + } + else { + const Camera& camera = wxGetApp().plater()->get_camera(); + const Vec4i viewport(camera.get_viewport().data()); + Vec3d out; + igl::unproject(Vec3d(mouse_pos.x(), viewport[3] - mouse_pos.y(), *z), camera.get_view_matrix().matrix(), camera.get_projection_matrix().matrix(), viewport, out); + return out; + } +#else + const Camera& camera = wxGetApp().plater()->get_camera(); + const Matrix4d modelview = camera.get_view_matrix().matrix(); + const Matrix4d projection = camera.get_projection_matrix().matrix(); + const Vec4i viewport(camera.get_viewport().data()); + + const int y = viewport[3] - mouse_pos.y(); + float mouse_z; + if (z == nullptr) + glsafe(::glReadPixels(mouse_pos.x(), y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, (void*)&mouse_z)); + else + mouse_z = *z; + + Vec3d out; + igl::unproject(Vec3d(mouse_pos.x(), y, mouse_z), modelview, projection, viewport, out); + return out; +#endif // ENABLE_RAYCAST_PICKING +} + +Vec3d GLCanvas3D::_mouse_to_bed_3d(const Point& mouse_pos) +{ + return mouse_ray(mouse_pos).intersect_plane(0.0); +} + +void GLCanvas3D::_start_timer() +{ + m_timer.Start(100, wxTIMER_CONTINUOUS); +} + +void GLCanvas3D::_stop_timer() +{ + m_timer.Stop(); +} + +void GLCanvas3D::_load_print_toolpaths(const BuildVolume &build_volume) +{ + const Print *print = this->fff_print(); + if (print == nullptr) + return; + + if (! print->is_step_done(psSkirtBrim)) + return; + + if (!print->has_skirt() && !print->has_brim()) + return; + + const ColorRGBA color = ColorRGBA::GREENISH(); + + // number of skirt layers + size_t total_layer_count = 0; + for (const PrintObject* print_object : print->objects()) { + total_layer_count = std::max(total_layer_count, print_object->total_layer_count()); + } + size_t skirt_height = print->has_infinite_skirt() ? total_layer_count : std::min(print->config().skirt_height.value, total_layer_count); + if (skirt_height == 0 && print->has_brim()) + skirt_height = 1; + + // Get first skirt_height layers. + //FIXME This code is fishy. It may not work for multiple objects with different layering due to variable layer height feature. + // This is not critical as this is just an initial preview. + const PrintObject* highest_object = *std::max_element(print->objects().begin(), print->objects().end(), [](auto l, auto r){ return l->layers().size() < r->layers().size(); }); + std::vector print_zs; + print_zs.reserve(skirt_height * 2); + for (size_t i = 0; i < std::min(skirt_height, highest_object->layers().size()); ++ i) + print_zs.emplace_back(float(highest_object->layers()[i]->print_z)); + // Only add skirt for the raft layers. + for (size_t i = 0; i < std::min(skirt_height, std::min(highest_object->slicing_parameters().raft_layers(), highest_object->support_layers().size())); ++ i) + print_zs.emplace_back(float(highest_object->support_layers()[i]->print_z)); + sort_remove_duplicates(print_zs); + skirt_height = std::min(skirt_height, print_zs.size()); + print_zs.erase(print_zs.begin() + skirt_height, print_zs.end()); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLVolume* volume = m_volumes.new_toolpath_volume(color); + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; +#else + GLVolume *volume = m_volumes.new_toolpath_volume(color, VERTEX_BUFFER_RESERVE_SIZE); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + for (size_t i = 0; i < skirt_height; ++ i) { + volume->print_zs.emplace_back(print_zs[i]); +#if ENABLE_LEGACY_OPENGL_REMOVAL + volume->offsets.emplace_back(init_data.indices_count()); + if (i == 0) + _3DScene::extrusionentity_to_verts(print->brim(), print_zs[i], Point(0, 0), init_data); + _3DScene::extrusionentity_to_verts(print->skirt(), print_zs[i], Point(0, 0), init_data); +#else + volume->offsets.emplace_back(volume->indexed_vertex_array.quad_indices.size()); + volume->offsets.emplace_back(volume->indexed_vertex_array.triangle_indices.size()); + if (i == 0) + _3DScene::extrusionentity_to_verts(print->brim(), print_zs[i], Point(0, 0), *volume); + _3DScene::extrusionentity_to_verts(print->skirt(), print_zs[i], Point(0, 0), *volume); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + // Ensure that no volume grows over the limits. If the volume is too large, allocate a new one. +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (init_data.vertices_size_bytes() > MAX_VERTEX_BUFFER_SIZE) { + volume->model.init_from(std::move(init_data)); +#else + if (volume->indexed_vertex_array.vertices_and_normals_interleaved.size() > MAX_VERTEX_BUFFER_SIZE) { +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + GLVolume &vol = *volume; + volume = m_volumes.new_toolpath_volume(vol.color); +#if !ENABLE_LEGACY_OPENGL_REMOVAL + reserve_new_volume_finalize_old_volume(*volume, vol, m_initialized); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + } + } +#if ENABLE_LEGACY_OPENGL_REMOVAL + volume->model.init_from(std::move(init_data)); + volume->is_outside = !contains(build_volume, volume->model); +#else + volume->is_outside = ! build_volume.all_paths_inside_vertices_and_normals_interleaved(volume->indexed_vertex_array.vertices_and_normals_interleaved, volume->indexed_vertex_array.bounding_box()); + volume->indexed_vertex_array.finalize_geometry(m_initialized); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, const BuildVolume& build_volume, const std::vector& str_tool_colors, const std::vector& color_print_values) +{ + std::vector tool_colors; + decode_colors(str_tool_colors, tool_colors); + + struct Ctxt + { + const PrintInstances *shifted_copies; + std::vector layers; + bool has_perimeters; + bool has_infill; + bool has_support; + const std::vector* tool_colors; + bool is_single_material_print; + int extruders_cnt; + const std::vector* color_print_values; + + static ColorRGBA color_perimeters() { return ColorRGBA::YELLOW(); } + static ColorRGBA color_infill() { return ColorRGBA::REDISH(); } + static ColorRGBA color_support() { return ColorRGBA::GREENISH(); } + static ColorRGBA color_pause_or_custom_code() { return ColorRGBA::GRAY(); } + + // For cloring by a tool, return a parsed color. + bool color_by_tool() const { return tool_colors != nullptr; } + size_t number_tools() const { return color_by_tool() ? tool_colors->size() : 0; } + const ColorRGBA& color_tool(size_t tool) const { return (*tool_colors)[tool]; } + + // For coloring by a color_print(M600), return a parsed color. + bool color_by_color_print() const { return color_print_values!=nullptr; } + const size_t color_print_color_idx_by_layer_idx(const size_t layer_idx) const { + const CustomGCode::Item value{layers[layer_idx]->print_z + EPSILON, CustomGCode::Custom, 0, ""}; + auto it = std::lower_bound(color_print_values->begin(), color_print_values->end(), value); + return (it - color_print_values->begin()) % number_tools(); + } + + const size_t color_print_color_idx_by_layer_idx_and_extruder(const size_t layer_idx, const int extruder) const + { + const coordf_t print_z = layers[layer_idx]->print_z; + + auto it = std::find_if(color_print_values->begin(), color_print_values->end(), + [print_z](const CustomGCode::Item& code) + { return fabs(code.print_z - print_z) < EPSILON; }); + if (it != color_print_values->end()) { + CustomGCode::Type type = it->type; + // pause print or custom Gcode + if (type == CustomGCode::PausePrint || + (type != CustomGCode::ColorChange && type != CustomGCode::ToolChange)) + return number_tools()-1; // last color item is a gray color for pause print or custom G-code + + // change tool (extruder) + if (type == CustomGCode::ToolChange) + return get_color_idx_for_tool_change(it, extruder); + // change color for current extruder + if (type == CustomGCode::ColorChange) { + int color_idx = get_color_idx_for_color_change(it, extruder); + if (color_idx >= 0) + return color_idx; + } + } + + const CustomGCode::Item value{print_z + EPSILON, CustomGCode::Custom, 0, ""}; + it = std::lower_bound(color_print_values->begin(), color_print_values->end(), value); + while (it != color_print_values->begin()) { + --it; + // change color for current extruder + if (it->type == CustomGCode::ColorChange) { + int color_idx = get_color_idx_for_color_change(it, extruder); + if (color_idx >= 0) + return color_idx; + } + // change tool (extruder) + if (it->type == CustomGCode::ToolChange) + return get_color_idx_for_tool_change(it, extruder); + } + + return std::min(extruders_cnt - 1, std::max(extruder - 1, 0));; + } + + private: + int get_m600_color_idx(std::vector::const_iterator it) const + { + int shift = 0; + while (it != color_print_values->begin()) { + --it; + if (it->type == CustomGCode::ColorChange) + shift++; + } + return extruders_cnt + shift; + } + + int get_color_idx_for_tool_change(std::vector::const_iterator it, const int extruder) const + { + const int current_extruder = it->extruder == 0 ? extruder : it->extruder; + if (number_tools() == size_t(extruders_cnt + 1)) // there is no one "M600" + return std::min(extruders_cnt - 1, std::max(current_extruder - 1, 0)); + + auto it_n = it; + while (it_n != color_print_values->begin()) { + --it_n; + if (it_n->type == CustomGCode::ColorChange && it_n->extruder == current_extruder) + return get_m600_color_idx(it_n); + } + + return std::min(extruders_cnt - 1, std::max(current_extruder - 1, 0)); + } + + int get_color_idx_for_color_change(std::vector::const_iterator it, const int extruder) const + { + if (extruders_cnt == 1) + return get_m600_color_idx(it); + + auto it_n = it; + bool is_tool_change = false; + while (it_n != color_print_values->begin()) { + --it_n; + if (it_n->type == CustomGCode::ToolChange) { + is_tool_change = true; + if (it_n->extruder == it->extruder || (it_n->extruder == 0 && it->extruder == extruder)) + return get_m600_color_idx(it); + break; + } + } + if (!is_tool_change && it->extruder == extruder) + return get_m600_color_idx(it); + + return -1; + } + + } ctxt; + + ctxt.has_perimeters = print_object.is_step_done(posPerimeters); + ctxt.has_infill = print_object.is_step_done(posInfill); + ctxt.has_support = print_object.is_step_done(posSupportMaterial); + ctxt.tool_colors = tool_colors.empty() ? nullptr : &tool_colors; + ctxt.color_print_values = color_print_values.empty() ? nullptr : &color_print_values; + ctxt.is_single_material_print = this->fff_print()->extruders().size()==1; + ctxt.extruders_cnt = wxGetApp().extruders_edited_cnt(); + + ctxt.shifted_copies = &print_object.instances(); + + // order layers by print_z + { + size_t nlayers = 0; + if (ctxt.has_perimeters || ctxt.has_infill) + nlayers = print_object.layers().size(); + if (ctxt.has_support) + nlayers += print_object.support_layers().size(); + ctxt.layers.reserve(nlayers); + } + if (ctxt.has_perimeters || ctxt.has_infill) + for (const Layer *layer : print_object.layers()) + ctxt.layers.emplace_back(layer); + if (ctxt.has_support) + for (const Layer *layer : print_object.support_layers()) + ctxt.layers.emplace_back(layer); + std::sort(ctxt.layers.begin(), ctxt.layers.end(), [](const Layer *l1, const Layer *l2) { return l1->print_z < l2->print_z; }); + + // Maximum size of an allocation block: 32MB / sizeof(float) + BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - start" << m_volumes.log_memory_info() << log_memory_info(); + + const bool is_selected_separate_extruder = m_selected_extruder > 0 && ctxt.color_by_color_print(); + + //FIXME Improve the heuristics for a grain size. + size_t grain_size = std::max(ctxt.layers.size() / 16, size_t(1)); + tbb::spin_mutex new_volume_mutex; + auto new_volume = [this, &new_volume_mutex](const ColorRGBA& color) { + // Allocate the volume before locking. + GLVolume *volume = new GLVolume(color); + volume->is_extrusion_path = true; +#if ENABLE_LEGACY_OPENGL_REMOVAL + // to prevent sending data to gpu (in the main thread) while + // editing the model geometry + volume->model.disable_render(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + tbb::spin_mutex::scoped_lock lock; + // Lock by ROII, so if the emplace_back() fails, the lock will be released. + lock.acquire(new_volume_mutex); + m_volumes.volumes.emplace_back(volume); + lock.release(); + return volume; + }; + const size_t volumes_cnt_initial = m_volumes.volumes.size(); + // Limit the number of threads as the code below does not scale well due to memory pressure. + // (most of the time is spent in malloc / free / memmove) + // Not using all the threads leaves some of the threads to G-code generator. + tbb::task_arena limited_arena(std::min(tbb::this_task_arena::max_concurrency(), 4)); + limited_arena.execute([&ctxt, grain_size, &new_volume, is_selected_separate_extruder, this]{ + tbb::parallel_for( + tbb::blocked_range(0, ctxt.layers.size(), grain_size), + [&ctxt, &new_volume, is_selected_separate_extruder, this](const tbb::blocked_range& range) { + GLVolumePtrs vols; +#if ENABLE_LEGACY_OPENGL_REMOVAL + std::vector geometries; + auto select_geometry = [&ctxt, &geometries](size_t layer_idx, int extruder, int feature) -> GLModel::Geometry& { + return geometries[ctxt.color_by_color_print() ? + ctxt.color_print_color_idx_by_layer_idx_and_extruder(layer_idx, extruder) : + ctxt.color_by_tool() ? + std::min(ctxt.number_tools() - 1, std::max(extruder - 1, 0)) : + feature + ]; + }; +#else + auto volume = [&ctxt, &vols](size_t layer_idx, int extruder, int feature) -> GLVolume& { + return *vols[ctxt.color_by_color_print() ? + ctxt.color_print_color_idx_by_layer_idx_and_extruder(layer_idx, extruder) : + ctxt.color_by_tool() ? + std::min(ctxt.number_tools() - 1, std::max(extruder - 1, 0)) : + feature + ]; + }; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + if (ctxt.color_by_color_print() || ctxt.color_by_tool()) { + for (size_t i = 0; i < ctxt.number_tools(); ++i) { + vols.emplace_back(new_volume(ctxt.color_tool(i))); +#if ENABLE_LEGACY_OPENGL_REMOVAL + geometries.emplace_back(GLModel::Geometry()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + } + else { + vols = { new_volume(ctxt.color_perimeters()), new_volume(ctxt.color_infill()), new_volume(ctxt.color_support()) }; +#if ENABLE_LEGACY_OPENGL_REMOVAL + geometries = { GLModel::Geometry(), GLModel::Geometry(), GLModel::Geometry() }; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + assert(vols.size() == geometries.size()); + for (GLModel::Geometry& g : geometries) { + g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + } +#else + for (GLVolume *vol : vols) + // Reserving number of vertices (3x position + 3x color) + vol->indexed_vertex_array.reserve(VERTEX_BUFFER_RESERVE_SIZE / 6); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) { + const Layer *layer = ctxt.layers[idx_layer]; + + if (is_selected_separate_extruder) { + bool at_least_one_has_correct_extruder = false; + for (const LayerRegion* layerm : layer->regions()) { + if (layerm->slices.surfaces.empty()) + continue; + const PrintRegionConfig& cfg = layerm->region().config(); + if (cfg.perimeter_extruder.value == m_selected_extruder || + cfg.infill_extruder.value == m_selected_extruder || + cfg.solid_infill_extruder.value == m_selected_extruder ) { + at_least_one_has_correct_extruder = true; + break; + } + } + if (!at_least_one_has_correct_extruder) + continue; + } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + for (size_t i = 0; i < vols.size(); ++i) { + GLVolume* vol = vols[i]; + if (vol->print_zs.empty() || vol->print_zs.back() != layer->print_z) { + vol->print_zs.emplace_back(layer->print_z); + vol->offsets.emplace_back(geometries[i].indices_count()); + } + } +#else + for (GLVolume* vol : vols) + if (vol->print_zs.empty() || vol->print_zs.back() != layer->print_z) { + vol->print_zs.emplace_back(layer->print_z); + vol->offsets.emplace_back(vol->indexed_vertex_array.quad_indices.size()); + vol->offsets.emplace_back(vol->indexed_vertex_array.triangle_indices.size()); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + for (const PrintInstance &instance : *ctxt.shifted_copies) { + const Point © = instance.shift; + for (const LayerRegion *layerm : layer->regions()) { + if (is_selected_separate_extruder) { + const PrintRegionConfig& cfg = layerm->region().config(); + if (cfg.perimeter_extruder.value != m_selected_extruder || + cfg.infill_extruder.value != m_selected_extruder || + cfg.solid_infill_extruder.value != m_selected_extruder) + continue; + } + if (ctxt.has_perimeters) +#if ENABLE_LEGACY_OPENGL_REMOVAL + _3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy, + select_geometry(idx_layer, layerm->region().config().perimeter_extruder.value, 0)); +#else + _3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy, + volume(idx_layer, layerm->region().config().perimeter_extruder.value, 0)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + if (ctxt.has_infill) { + for (const ExtrusionEntity *ee : layerm->fills.entities) { + // fill represents infill extrusions of a single island. + const auto *fill = dynamic_cast(ee); + if (! fill->entities.empty()) +#if ENABLE_LEGACY_OPENGL_REMOVAL + _3DScene::extrusionentity_to_verts(*fill, float(layer->print_z), copy, + select_geometry(idx_layer, is_solid_infill(fill->entities.front()->role()) ? + layerm->region().config().solid_infill_extruder : + layerm->region().config().infill_extruder, 1)); +#else + _3DScene::extrusionentity_to_verts(*fill, float(layer->print_z), copy, + volume(idx_layer, + is_solid_infill(fill->entities.front()->role()) ? + layerm->region().config().solid_infill_extruder : + layerm->region().config().infill_extruder, + 1)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + } + } + if (ctxt.has_support) { + const SupportLayer *support_layer = dynamic_cast(layer); + if (support_layer) { + for (const ExtrusionEntity *extrusion_entity : support_layer->support_fills.entities) +#if ENABLE_LEGACY_OPENGL_REMOVAL + _3DScene::extrusionentity_to_verts(extrusion_entity, float(layer->print_z), copy, + select_geometry(idx_layer, (extrusion_entity->role() == erSupportMaterial) ? + support_layer->object()->config().support_material_extruder : + support_layer->object()->config().support_material_interface_extruder, 2)); +#else + _3DScene::extrusionentity_to_verts(extrusion_entity, float(layer->print_z), copy, + volume(idx_layer, + (extrusion_entity->role() == erSupportMaterial) ? + support_layer->object()->config().support_material_extruder : + support_layer->object()->config().support_material_interface_extruder, + 2)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + } + } + // Ensure that no volume grows over the limits. If the volume is too large, allocate a new one. + for (size_t i = 0; i < vols.size(); ++i) { + GLVolume &vol = *vols[i]; +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (geometries[i].vertices_size_bytes() > MAX_VERTEX_BUFFER_SIZE) { + vol.model.init_from(std::move(geometries[i])); +#else + if (vol.indexed_vertex_array.vertices_and_normals_interleaved.size() > MAX_VERTEX_BUFFER_SIZE) { +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + vols[i] = new_volume(vol.color); +#if !ENABLE_LEGACY_OPENGL_REMOVAL + reserve_new_volume_finalize_old_volume(*vols[i], vol, false); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + } + } + } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + for (size_t i = 0; i < vols.size(); ++i) { + if (!geometries[i].is_empty()) + vols[i]->model.init_from(std::move(geometries[i])); + } +#else + for (GLVolume *vol : vols) + // Ideally one would call vol->indexed_vertex_array.finalize() here to move the buffers to the OpenGL driver, + // but this code runs in parallel and the OpenGL driver is not thread safe. + vol->indexed_vertex_array.shrink_to_fit(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + }); + }); // task arena + + BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - finalizing results" << m_volumes.log_memory_info() << log_memory_info(); + // Remove empty volumes from the newly added volumes. + { + for (auto ptr_it = m_volumes.volumes.begin() + volumes_cnt_initial; ptr_it != m_volumes.volumes.end(); ++ptr_it) + if ((*ptr_it)->empty()) { + delete *ptr_it; + *ptr_it = nullptr; + } + m_volumes.volumes.erase(std::remove(m_volumes.volumes.begin() + volumes_cnt_initial, m_volumes.volumes.end(), nullptr), m_volumes.volumes.end()); + } + for (size_t i = volumes_cnt_initial; i < m_volumes.volumes.size(); ++i) { + GLVolume* v = m_volumes.volumes[i]; +#if ENABLE_LEGACY_OPENGL_REMOVAL + v->is_outside = !contains(build_volume, v->model); + // We are done editinig the model, now it can be sent to gpu + v->model.enable_render(); +#else + v->is_outside = ! build_volume.all_paths_inside_vertices_and_normals_interleaved(v->indexed_vertex_array.vertices_and_normals_interleaved, v->indexed_vertex_array.bounding_box()); + v->indexed_vertex_array.finalize_geometry(m_initialized); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + + BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - end" << m_volumes.log_memory_info() << log_memory_info(); +} + +void GLCanvas3D::_load_wipe_tower_toolpaths(const BuildVolume& build_volume, const std::vector& str_tool_colors) +{ + const Print *print = this->fff_print(); + if (print == nullptr || print->wipe_tower_data().tool_changes.empty()) + return; + + if (!print->is_step_done(psWipeTower)) + return; + + std::vector tool_colors; + decode_colors(str_tool_colors, tool_colors); + + struct Ctxt + { + const Print *print; + const std::vector *tool_colors; + Vec2f wipe_tower_pos; + float wipe_tower_angle; + + static ColorRGBA color_support() { return ColorRGBA::GREENISH(); } + + // For cloring by a tool, return a parsed color. + bool color_by_tool() const { return tool_colors != nullptr; } + size_t number_tools() const { return this->color_by_tool() ? tool_colors->size() : 0; } + const ColorRGBA& color_tool(size_t tool) const { return (*tool_colors)[tool]; } + int volume_idx(int tool, int feature) const { + return this->color_by_tool() ? std::min(this->number_tools() - 1, std::max(tool, 0)) : feature; + } + + const std::vector& tool_change(size_t idx) { + const auto &tool_changes = print->wipe_tower_data().tool_changes; + return priming.empty() ? + ((idx == tool_changes.size()) ? final : tool_changes[idx]) : + ((idx == 0) ? priming : (idx == tool_changes.size() + 1) ? final : tool_changes[idx - 1]); + } + std::vector priming; + std::vector final; + } ctxt; + + ctxt.print = print; + ctxt.tool_colors = tool_colors.empty() ? nullptr : &tool_colors; + if (print->wipe_tower_data().priming && print->config().single_extruder_multi_material_priming) + for (int i=0; i<(int)print->wipe_tower_data().priming.get()->size(); ++i) + ctxt.priming.emplace_back(print->wipe_tower_data().priming.get()->at(i)); + if (print->wipe_tower_data().final_purge) + ctxt.final.emplace_back(*print->wipe_tower_data().final_purge.get()); + + ctxt.wipe_tower_angle = ctxt.print->config().wipe_tower_rotation_angle.value/180.f * PI; + ctxt.wipe_tower_pos = Vec2f(ctxt.print->config().wipe_tower_x.value, ctxt.print->config().wipe_tower_y.value); + + BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - start" << m_volumes.log_memory_info() << log_memory_info(); + + //FIXME Improve the heuristics for a grain size. + size_t n_items = print->wipe_tower_data().tool_changes.size() + (ctxt.priming.empty() ? 0 : 1); + size_t grain_size = std::max(n_items / 128, size_t(1)); + tbb::spin_mutex new_volume_mutex; + auto new_volume = [this, &new_volume_mutex](const ColorRGBA& color) { + auto *volume = new GLVolume(color); + volume->is_extrusion_path = true; +#if ENABLE_LEGACY_OPENGL_REMOVAL + // to prevent sending data to gpu (in the main thread) while + // editing the model geometry + volume->model.disable_render(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + tbb::spin_mutex::scoped_lock lock; + lock.acquire(new_volume_mutex); + m_volumes.volumes.emplace_back(volume); + lock.release(); + return volume; + }; + const size_t volumes_cnt_initial = m_volumes.volumes.size(); + std::vector volumes_per_thread(n_items); + tbb::parallel_for( + tbb::blocked_range(0, n_items, grain_size), + [&ctxt, &new_volume](const tbb::blocked_range& range) { + // Bounding box of this slab of a wipe tower. + GLVolumePtrs vols; +#if ENABLE_LEGACY_OPENGL_REMOVAL + std::vector geometries; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + if (ctxt.color_by_tool()) { + for (size_t i = 0; i < ctxt.number_tools(); ++i) { + vols.emplace_back(new_volume(ctxt.color_tool(i))); +#if ENABLE_LEGACY_OPENGL_REMOVAL + geometries.emplace_back(GLModel::Geometry()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + } + else { + vols = { new_volume(ctxt.color_support()) }; +#if ENABLE_LEGACY_OPENGL_REMOVAL + geometries = { GLModel::Geometry() }; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + assert(vols.size() == geometries.size()); + for (GLModel::Geometry& g : geometries) { + g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + } +#else + for (GLVolume *volume : vols) + // Reserving number of vertices (3x position + 3x color) + volume->indexed_vertex_array.reserve(VERTEX_BUFFER_RESERVE_SIZE / 6); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) { + const std::vector &layer = ctxt.tool_change(idx_layer); + for (size_t i = 0; i < vols.size(); ++i) { + GLVolume &vol = *vols[i]; + if (vol.print_zs.empty() || vol.print_zs.back() != layer.front().print_z) { + vol.print_zs.emplace_back(layer.front().print_z); +#if ENABLE_LEGACY_OPENGL_REMOVAL + vol.offsets.emplace_back(geometries[i].indices_count()); +#else + vol.offsets.emplace_back(vol.indexed_vertex_array.quad_indices.size()); + vol.offsets.emplace_back(vol.indexed_vertex_array.triangle_indices.size()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + } + for (const WipeTower::ToolChangeResult &extrusions : layer) { + for (size_t i = 1; i < extrusions.extrusions.size();) { + const WipeTower::Extrusion &e = extrusions.extrusions[i]; + if (e.width == 0.) { + ++i; + continue; + } + size_t j = i + 1; + if (ctxt.color_by_tool()) + for (; j < extrusions.extrusions.size() && extrusions.extrusions[j].tool == e.tool && extrusions.extrusions[j].width > 0.f; ++j); + else + for (; j < extrusions.extrusions.size() && extrusions.extrusions[j].width > 0.f; ++j); + size_t n_lines = j - i; + Lines lines; + std::vector widths; + std::vector heights; + lines.reserve(n_lines); + widths.reserve(n_lines); + heights.assign(n_lines, extrusions.layer_height); + WipeTower::Extrusion e_prev = extrusions.extrusions[i-1]; + + if (!extrusions.priming) { // wipe tower extrusions describe the wipe tower at the origin with no rotation + e_prev.pos = Eigen::Rotation2Df(ctxt.wipe_tower_angle) * e_prev.pos; + e_prev.pos += ctxt.wipe_tower_pos; + } + + for (; i < j; ++i) { + WipeTower::Extrusion e = extrusions.extrusions[i]; + assert(e.width > 0.f); + if (!extrusions.priming) { + e.pos = Eigen::Rotation2Df(ctxt.wipe_tower_angle) * e.pos; + e.pos += ctxt.wipe_tower_pos; + } + + lines.emplace_back(Point::new_scale(e_prev.pos.x(), e_prev.pos.y()), Point::new_scale(e.pos.x(), e.pos.y())); + widths.emplace_back(e.width); + + e_prev = e; + } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + _3DScene::thick_lines_to_verts(lines, widths, heights, lines.front().a == lines.back().b, extrusions.print_z, + geometries[ctxt.volume_idx(e.tool, 0)]); +#else + _3DScene::thick_lines_to_verts(lines, widths, heights, lines.front().a == lines.back().b, extrusions.print_z, + *vols[ctxt.volume_idx(e.tool, 0)]); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + } + } + for (size_t i = 0; i < vols.size(); ++i) { + GLVolume &vol = *vols[i]; +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (geometries[i].vertices_size_bytes() > MAX_VERTEX_BUFFER_SIZE) { + vol.model.init_from(std::move(geometries[i])); +#else + if (vol.indexed_vertex_array.vertices_and_normals_interleaved.size() > MAX_VERTEX_BUFFER_SIZE) { +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + vols[i] = new_volume(vol.color); +#if !ENABLE_LEGACY_OPENGL_REMOVAL + reserve_new_volume_finalize_old_volume(*vols[i], vol, false); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + } + } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + for (size_t i = 0; i < vols.size(); ++i) { + if (!geometries[i].is_empty()) + vols[i]->model.init_from(std::move(geometries[i])); + } +#else + for (GLVolume *vol : vols) + vol->indexed_vertex_array.shrink_to_fit(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + }); + + BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - finalizing results" << m_volumes.log_memory_info() << log_memory_info(); + // Remove empty volumes from the newly added volumes. + { + for (auto ptr_it = m_volumes.volumes.begin() + volumes_cnt_initial; ptr_it != m_volumes.volumes.end(); ++ptr_it) + if ((*ptr_it)->empty()) { + delete *ptr_it; + *ptr_it = nullptr; + } + m_volumes.volumes.erase(std::remove(m_volumes.volumes.begin() + volumes_cnt_initial, m_volumes.volumes.end(), nullptr), m_volumes.volumes.end()); + } + for (size_t i = volumes_cnt_initial; i < m_volumes.volumes.size(); ++i) { + GLVolume* v = m_volumes.volumes[i]; +#if ENABLE_LEGACY_OPENGL_REMOVAL + v->is_outside = !contains(build_volume, v->model); + // We are done editinig the model, now it can be sent to gpu + v->model.enable_render(); +#else + v->is_outside = ! build_volume.all_paths_inside_vertices_and_normals_interleaved(v->indexed_vertex_array.vertices_and_normals_interleaved, v->indexed_vertex_array.bounding_box()); + v->indexed_vertex_array.finalize_geometry(m_initialized); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + + BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - end" << m_volumes.log_memory_info() << log_memory_info(); +} + +// While it looks like we can call +// this->reload_scene(true, true) +// the two functions are quite different: +// 1) This function only loads objects, for which the step slaposSliceSupports already finished. Therefore objects outside of the print bed never load. +// 2) This function loads object mesh with the relative scaling correction (the "relative_correction" parameter) was applied, +// therefore the mesh may be slightly larger or smaller than the mesh shown in the 3D scene. +void GLCanvas3D::_load_sla_shells() +{ + const SLAPrint* print = this->sla_print(); + if (print->objects().empty()) + // nothing to render, return + return; + + auto add_volume = [this](const SLAPrintObject &object, int volume_id, const SLAPrintObject::Instance& instance, + const TriangleMesh& mesh, const ColorRGBA& color, bool outside_printer_detection_enabled) { + m_volumes.volumes.emplace_back(new GLVolume(color)); + GLVolume& v = *m_volumes.volumes.back(); +#if ENABLE_SMOOTH_NORMALS +#if ENABLE_LEGACY_OPENGL_REMOVAL + v.model.init_from(mesh, true); +#else + v.indexed_vertex_array.load_mesh(mesh, true); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#else +#if ENABLE_LEGACY_OPENGL_REMOVAL + v.model.init_from(mesh); +#else + v.indexed_vertex_array.load_mesh(mesh); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#endif // ENABLE_SMOOTH_NORMALS +#if !ENABLE_LEGACY_OPENGL_REMOVAL + v.indexed_vertex_array.finalize_geometry(m_initialized); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + v.shader_outside_printer_detection_enabled = outside_printer_detection_enabled; + v.composite_id.volume_id = volume_id; + v.set_instance_offset(unscale(instance.shift.x(), instance.shift.y(), 0.0)); + v.set_instance_rotation({ 0.0, 0.0, (double)instance.rotation }); + v.set_instance_mirror(X, object.is_left_handed() ? -1. : 1.); + v.set_convex_hull(mesh.convex_hull_3d()); + }; + + // adds objects' volumes + for (const SLAPrintObject* obj : print->objects()) + if (obj->is_step_done(slaposSliceSupports)) { + unsigned int initial_volumes_count = (unsigned int)m_volumes.volumes.size(); + for (const SLAPrintObject::Instance& instance : obj->instances()) { + add_volume(*obj, 0, instance, obj->get_mesh_to_print(), GLVolume::MODEL_COLOR[0], true); + // Set the extruder_id and volume_id to achieve the same color as in the 3D scene when + // through the update_volumes_colors_by_extruder() call. + m_volumes.volumes.back()->extruder_id = obj->model_object()->volumes.front()->extruder_id(); + if (obj->is_step_done(slaposSupportTree) && obj->has_mesh(slaposSupportTree)) + add_volume(*obj, -int(slaposSupportTree), instance, obj->support_mesh(), GLVolume::SLA_SUPPORT_COLOR, true); + if (obj->is_step_done(slaposPad) && obj->has_mesh(slaposPad)) + add_volume(*obj, -int(slaposPad), instance, obj->pad_mesh(), GLVolume::SLA_PAD_COLOR, false); + } + double shift_z = obj->get_current_elevation(); + for (unsigned int i = initial_volumes_count; i < m_volumes.volumes.size(); ++ i) { + // apply shift z + m_volumes.volumes[i]->set_sla_shift_z(shift_z); + } + } + + update_volumes_colors_by_extruder(); +} + +void GLCanvas3D::_update_sla_shells_outside_state() +{ + check_volumes_outside_state(); +} + +void GLCanvas3D::_set_warning_notification_if_needed(EWarning warning) +{ + _set_current(); + bool show = false; + if (!m_volumes.empty()) { + if (current_printer_technology() == ptSLA) { + const auto [res, volume] = _is_any_volume_outside(); + if (res) { + if (warning == EWarning::ObjectClashed) + show = !volume->is_sla_support(); + else if (warning == EWarning::SlaSupportsOutside) + show = volume->is_sla_support(); + } + } + else + show = _is_any_volume_outside().first; + } + else { + if (wxGetApp().is_editor()) { + if (current_printer_technology() != ptSLA) + show = m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed(); + } + } + + _set_warning_notification(warning, show); +} + +void GLCanvas3D::_set_warning_notification(EWarning warning, bool state) +{ + enum ErrorType{ + PLATER_WARNING, + PLATER_ERROR, + SLICING_ERROR + }; + std::string text; + ErrorType error = ErrorType::PLATER_WARNING; + switch (warning) { + case EWarning::ObjectOutside: text = _u8L("An object outside the print area was detected."); break; + case EWarning::ToolpathOutside: text = _u8L("A toolpath outside the print area was detected."); error = ErrorType::SLICING_ERROR; break; + case EWarning::SlaSupportsOutside: text = _u8L("SLA supports outside the print area were detected."); error = ErrorType::PLATER_ERROR; break; + case EWarning::SomethingNotShown: text = _u8L("Some objects are not visible during editing."); break; + case EWarning::ObjectClashed: + text = _u8L("An object outside the print area was detected.\n" + "Resolve the current problem to continue slicing."); + error = ErrorType::PLATER_ERROR; + break; + } + auto& notification_manager = *wxGetApp().plater()->get_notification_manager(); + switch (error) + { + case PLATER_WARNING: + if (state) + notification_manager.push_plater_warning_notification(text); + else + notification_manager.close_plater_warning_notification(text); + break; + case PLATER_ERROR: + if (state) + notification_manager.push_plater_error_notification(text); + else + notification_manager.close_plater_error_notification(text); + break; + case SLICING_ERROR: + if (state) + notification_manager.push_slicing_error_notification(text); + else + notification_manager.close_slicing_error_notification(text); + break; + default: + break; + } +} + +std::pair GLCanvas3D::_is_any_volume_outside() const +{ + for (const GLVolume* volume : m_volumes.volumes) { + if (volume != nullptr && volume->is_outside) + return std::make_pair(true, volume); + } + + return std::make_pair(false, nullptr); +} + +void GLCanvas3D::_update_selection_from_hover() +{ + bool ctrl_pressed = wxGetKeyState(WXK_CONTROL); + bool selection_changed = false; + + if (m_hover_volume_idxs.empty()) { + if (!ctrl_pressed && m_rectangle_selection.get_state() == GLSelectionRectangle::EState::Select) { + selection_changed = ! m_selection.is_empty(); + m_selection.remove_all(); + } + } + + GLSelectionRectangle::EState state = m_rectangle_selection.get_state(); + + bool hover_modifiers_only = true; + for (int i : m_hover_volume_idxs) { + if (!m_volumes.volumes[i]->is_modifier) { + hover_modifiers_only = false; + break; + } + } + + if (!m_rectangle_selection.is_empty()) { + if (state == GLSelectionRectangle::EState::Select) { + bool contains_all = true; + for (int i : m_hover_volume_idxs) { + if (!m_selection.contains_volume((unsigned int)i)) { + contains_all = false; + break; + } + } + + // the selection is going to be modified (Add) + if (!contains_all) { + wxGetApp().plater()->take_snapshot(_L("Selection-Add from rectangle"), UndoRedo::SnapshotType::Selection); + selection_changed = true; + } + } + else { + bool contains_any = false; + for (int i : m_hover_volume_idxs) { + if (m_selection.contains_volume((unsigned int)i)) { + contains_any = true; + break; + } + } + + // the selection is going to be modified (Remove) + if (contains_any) { + wxGetApp().plater()->take_snapshot(_L("Selection-Remove from rectangle"), UndoRedo::SnapshotType::Selection); + selection_changed = true; + } + } + } + + if (!selection_changed) + return; + + Plater::SuppressSnapshots suppress(wxGetApp().plater()); + + if (state == GLSelectionRectangle::EState::Select && !ctrl_pressed) + m_selection.clear(); + + for (int i : m_hover_volume_idxs) { + if (state == GLSelectionRectangle::EState::Select) { + if (hover_modifiers_only) { + const GLVolume& v = *m_volumes.volumes[i]; + m_selection.add_volume(v.object_idx(), v.volume_idx(), v.instance_idx(), false); + } + else + m_selection.add(i, false); + } + else + m_selection.remove(i); + } + + if (m_selection.is_empty()) + m_gizmos.reset_all_states(); + else + m_gizmos.refresh_on_off_state(); + + m_gizmos.update_data(); + post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); + m_dirty = true; +} + +bool GLCanvas3D::_deactivate_undo_redo_toolbar_items() +{ + if (m_undoredo_toolbar.is_item_pressed("undo")) { + m_undoredo_toolbar.force_right_action(m_undoredo_toolbar.get_item_id("undo"), *this); + return true; + } + else if (m_undoredo_toolbar.is_item_pressed("redo")) { + m_undoredo_toolbar.force_right_action(m_undoredo_toolbar.get_item_id("redo"), *this); + return true; + } + + return false; +} + +bool GLCanvas3D::is_search_pressed() const +{ + return m_main_toolbar.is_item_pressed("search"); +} + +bool GLCanvas3D::_deactivate_arrange_menu() +{ + if (m_main_toolbar.is_item_pressed("arrange")) { + m_main_toolbar.force_right_action(m_main_toolbar.get_item_id("arrange"), *this); + return true; + } + + return false; +} + +bool GLCanvas3D::_deactivate_search_toolbar_item() +{ + if (is_search_pressed()) { + m_main_toolbar.force_left_action(m_main_toolbar.get_item_id("search"), *this); + return true; + } + + return false; +} + +bool GLCanvas3D::_activate_search_toolbar_item() +{ + if (!m_main_toolbar.is_item_pressed("search")) { + m_main_toolbar.force_left_action(m_main_toolbar.get_item_id("search"), *this); + return true; + } + + return false; +} + +bool GLCanvas3D::_deactivate_collapse_toolbar_items() +{ + GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar(); + if (collapse_toolbar.is_item_pressed("print")) { + collapse_toolbar.force_left_action(collapse_toolbar.get_item_id("print"), *this); + return true; + } + + return false; +} + +void GLCanvas3D::highlight_toolbar_item(const std::string& item_name) +{ + GLToolbarItem* item = m_main_toolbar.get_item(item_name); + if (!item) + item = m_undoredo_toolbar.get_item(item_name); + if (!item || !item->is_visible()) + return; + m_toolbar_highlighter.init(item, this); +} + +void GLCanvas3D::highlight_gizmo(const std::string& gizmo_name) +{ + GLGizmosManager::EType gizmo = m_gizmos.get_gizmo_from_name(gizmo_name); + if(gizmo == GLGizmosManager::EType::Undefined) + return; + m_gizmo_highlighter.init(&m_gizmos, gizmo, this); +} + +const Print* GLCanvas3D::fff_print() const +{ + return (m_process == nullptr) ? nullptr : m_process->fff_print(); +} + +const SLAPrint* GLCanvas3D::sla_print() const +{ + return (m_process == nullptr) ? nullptr : m_process->sla_print(); +} + +void GLCanvas3D::WipeTowerInfo::apply_wipe_tower() const +{ + DynamicPrintConfig cfg; + cfg.opt("wipe_tower_x", true)->value = m_pos(X); + cfg.opt("wipe_tower_y", true)->value = m_pos(Y); + cfg.opt("wipe_tower_rotation_angle", true)->value = (180./M_PI) * m_rotation; + wxGetApp().get_tab(Preset::TYPE_PRINT)->load_config(cfg); +} + +void GLCanvas3D::RenderTimer::Notify() +{ + wxPostEvent((wxEvtHandler*)GetOwner(), RenderTimerEvent( EVT_GLCANVAS_RENDER_TIMER, *this)); +} + +void GLCanvas3D::ToolbarHighlighterTimer::Notify() +{ + wxPostEvent((wxEvtHandler*)GetOwner(), ToolbarHighlighterTimerEvent(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, *this)); +} + +void GLCanvas3D::GizmoHighlighterTimer::Notify() +{ + wxPostEvent((wxEvtHandler*)GetOwner(), GizmoHighlighterTimerEvent(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, *this)); +} + +void GLCanvas3D::ToolbarHighlighter::set_timer_owner(wxEvtHandler* owner, int timerid/* = wxID_ANY*/) +{ + m_timer.SetOwner(owner, timerid); +} + +void GLCanvas3D::ToolbarHighlighter::init(GLToolbarItem* toolbar_item, GLCanvas3D* canvas) +{ + if (m_timer.IsRunning()) + invalidate(); + if (!toolbar_item || !canvas) + return; + + m_timer.Start(300, false); + + m_toolbar_item = toolbar_item; + m_canvas = canvas; +} + +void GLCanvas3D::ToolbarHighlighter::invalidate() +{ + m_timer.Stop(); + + if (m_toolbar_item) { + m_toolbar_item->set_highlight(GLToolbarItem::EHighlightState::NotHighlighted); + } + m_toolbar_item = nullptr; + m_blink_counter = 0; + m_render_arrow = false; +} + +void GLCanvas3D::ToolbarHighlighter::blink() +{ + if (m_toolbar_item) { + char state = m_toolbar_item->get_highlight(); + if (state != (char)GLToolbarItem::EHighlightState::HighlightedShown) + m_toolbar_item->set_highlight(GLToolbarItem::EHighlightState::HighlightedShown); + else + m_toolbar_item->set_highlight(GLToolbarItem::EHighlightState::HighlightedHidden); + + m_render_arrow = !m_render_arrow; + m_canvas->set_as_dirty(); + } + else + invalidate(); + + if ((++m_blink_counter) >= 11) + invalidate(); +} + +void GLCanvas3D::GizmoHighlighter::set_timer_owner(wxEvtHandler* owner, int timerid/* = wxID_ANY*/) +{ + m_timer.SetOwner(owner, timerid); +} + +void GLCanvas3D::GizmoHighlighter::init(GLGizmosManager* manager, GLGizmosManager::EType gizmo, GLCanvas3D* canvas) +{ + if (m_timer.IsRunning()) + invalidate(); + if (!gizmo || !canvas) + return; + + m_timer.Start(300, false); + + m_gizmo_manager = manager; + m_gizmo_type = gizmo; + m_canvas = canvas; +} + +void GLCanvas3D::GizmoHighlighter::invalidate() +{ + m_timer.Stop(); + + if (m_gizmo_manager) { + m_gizmo_manager->set_highlight(GLGizmosManager::EType::Undefined, false); + } + m_gizmo_manager = nullptr; + m_gizmo_type = GLGizmosManager::EType::Undefined; + m_blink_counter = 0; + m_render_arrow = false; +} + +void GLCanvas3D::GizmoHighlighter::blink() +{ + if (m_gizmo_manager) { + if (m_blink_counter % 2 == 0) + m_gizmo_manager->set_highlight(m_gizmo_type, true); + else + m_gizmo_manager->set_highlight(m_gizmo_type, false); + + m_render_arrow = !m_render_arrow; + m_canvas->set_as_dirty(); + } + else + invalidate(); + + if ((++m_blink_counter) >= 11) + invalidate(); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index b03010b17..795a2a9a3 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1,3267 +1,3274 @@ -#include "libslic3r/Technologies.hpp" -#include "GUI_App.hpp" -#include "GUI_Init.hpp" -#include "GUI_ObjectList.hpp" -#include "GUI_ObjectManipulation.hpp" -#include "GUI_Factories.hpp" -#include "format.hpp" -#include "I18N.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "libslic3r/Utils.hpp" -#include "libslic3r/Model.hpp" -#include "libslic3r/I18N.hpp" -#include "libslic3r/PresetBundle.hpp" -#include "libslic3r/Color.hpp" - -#include "GUI.hpp" -#include "GUI_Utils.hpp" -#include "3DScene.hpp" -#include "MainFrame.hpp" -#include "Plater.hpp" -#include "GLCanvas3D.hpp" - -#include "../Utils/PresetUpdater.hpp" -#include "../Utils/PrintHost.hpp" -#include "../Utils/Process.hpp" -#include "../Utils/MacDarkMode.hpp" -#include "../Utils/AppUpdater.hpp" -#include "../Utils/WinRegistry.hpp" -#include "slic3r/Config/Snapshot.hpp" -#include "ConfigSnapshotDialog.hpp" -#include "FirmwareDialog.hpp" -#include "Preferences.hpp" -#include "Tab.hpp" -#include "SysInfoDialog.hpp" -#include "KBShortcutsDialog.hpp" -#include "UpdateDialogs.hpp" -#include "Mouse3DController.hpp" -#include "RemovableDriveManager.hpp" -#include "InstanceCheck.hpp" -#include "NotificationManager.hpp" -#include "UnsavedChangesDialog.hpp" -#include "SavePresetDialog.hpp" -#include "PrintHostDialogs.hpp" -#include "DesktopIntegrationDialog.hpp" -#include "SendSystemInfoDialog.hpp" - -#include "BitmapCache.hpp" -#include "Notebook.hpp" - -#ifdef __WXMSW__ -#include -#include -#ifdef _MSW_DARK_MODE -#include -#endif // _MSW_DARK_MODE -#endif -#ifdef _WIN32 -#include -#endif - -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG -#include -#include -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG - -// Needed for forcing menu icons back under gtk2 and gtk3 -#if defined(__WXGTK20__) || defined(__WXGTK3__) - #include -#endif - -using namespace std::literals; - -namespace Slic3r { -namespace GUI { - -class MainFrame; - -class SplashScreen : public wxSplashScreen -{ -public: - SplashScreen(const wxBitmap& bitmap, long splashStyle, int milliseconds, wxPoint pos = wxDefaultPosition) - : wxSplashScreen(bitmap, splashStyle, milliseconds, static_cast(wxGetApp().mainframe), wxID_ANY, wxDefaultPosition, wxDefaultSize, -#ifdef __APPLE__ - wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR | wxSTAY_ON_TOP -#else - wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR -#endif // !__APPLE__ - ) - { - wxASSERT(bitmap.IsOk()); - -// int init_dpi = get_dpi_for_window(this); - this->SetPosition(pos); - // The size of the SplashScreen can be hanged after its moving to another display - // So, update it from a bitmap size - this->SetClientSize(bitmap.GetWidth(), bitmap.GetHeight()); - this->CenterOnScreen(); -// int new_dpi = get_dpi_for_window(this); - -// m_scale = (float)(new_dpi) / (float)(init_dpi); - m_main_bitmap = bitmap; - -// scale_bitmap(m_main_bitmap, m_scale); - - // init constant texts and scale fonts - init_constant_text(); - - // this font will be used for the action string - m_action_font = m_constant_text.credits_font.Bold(); - - // draw logo and constant info text - Decorate(m_main_bitmap); - } - - void SetText(const wxString& text) - { - set_bitmap(m_main_bitmap); - if (!text.empty()) { - wxBitmap bitmap(m_main_bitmap); - - wxMemoryDC memDC; - memDC.SelectObject(bitmap); - - memDC.SetFont(m_action_font); - memDC.SetTextForeground(wxColour(237, 107, 33)); - memDC.DrawText(text, int(m_scale * 60), m_action_line_y_position); - - memDC.SelectObject(wxNullBitmap); - set_bitmap(bitmap); -#ifdef __WXOSX__ - // without this code splash screen wouldn't be updated under OSX - wxYield(); -#endif - } - } - - static wxBitmap MakeBitmap(wxBitmap bmp) - { - if (!bmp.IsOk()) - return wxNullBitmap; - - // create dark grey background for the splashscreen - // It will be 5/3 of the weight of the bitmap - int width = lround((double)5 / 3 * bmp.GetWidth()); - int height = bmp.GetHeight(); - - wxImage image(width, height); - unsigned char* imgdata_ = image.GetData(); - for (int i = 0; i < width * height; ++i) { - *imgdata_++ = 51; - *imgdata_++ = 51; - *imgdata_++ = 51; - } - - wxBitmap new_bmp(image); - - wxMemoryDC memDC; - memDC.SelectObject(new_bmp); - memDC.DrawBitmap(bmp, width - bmp.GetWidth(), 0, true); - - return new_bmp; - } - - void Decorate(wxBitmap& bmp) - { - if (!bmp.IsOk()) - return; - - // draw text to the box at the left of the splashscreen. - // this box will be 2/5 of the weight of the bitmap, and be at the left. - int width = lround(bmp.GetWidth() * 0.4); - - // load bitmap for logo - BitmapCache bmp_cache; - int logo_size = lround(width * 0.25); - wxBitmap logo_bmp = *bmp_cache.load_svg(wxGetApp().logo_name(), logo_size, logo_size); - - wxCoord margin = int(m_scale * 20); - - wxRect banner_rect(wxPoint(0, logo_size), wxPoint(width, bmp.GetHeight())); - banner_rect.Deflate(margin, 2 * margin); - - // use a memory DC to draw directly onto the bitmap - wxMemoryDC memDc(bmp); - - // draw logo - memDc.DrawBitmap(logo_bmp, margin, margin, true); - - // draw the (white) labels inside of our black box (at the left of the splashscreen) - memDc.SetTextForeground(wxColour(255, 255, 255)); - - memDc.SetFont(m_constant_text.title_font); - memDc.DrawLabel(m_constant_text.title, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); - - int title_height = memDc.GetTextExtent(m_constant_text.title).GetY(); - banner_rect.SetTop(banner_rect.GetTop() + title_height); - banner_rect.SetHeight(banner_rect.GetHeight() - title_height); - - memDc.SetFont(m_constant_text.version_font); - memDc.DrawLabel(m_constant_text.version, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); - int version_height = memDc.GetTextExtent(m_constant_text.version).GetY(); - - memDc.SetFont(m_constant_text.credits_font); - memDc.DrawLabel(m_constant_text.credits, banner_rect, wxALIGN_BOTTOM | wxALIGN_LEFT); - int credits_height = memDc.GetMultiLineTextExtent(m_constant_text.credits).GetY(); - int text_height = memDc.GetTextExtent("text").GetY(); - - // calculate position for the dynamic text - int logo_and_header_height = margin + logo_size + title_height + version_height; - m_action_line_y_position = logo_and_header_height + 0.5 * (bmp.GetHeight() - margin - credits_height - logo_and_header_height - text_height); - } - -private: - wxBitmap m_main_bitmap; - wxFont m_action_font; - int m_action_line_y_position; - float m_scale {1.0}; - - struct ConstantText - { - wxString title; - wxString version; - wxString credits; - - wxFont title_font; - wxFont version_font; - wxFont credits_font; - - void init(wxFont init_font) - { - // title - title = wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME; - - // dynamically get the version to display - version = _L("Version") + " " + std::string(SLIC3R_VERSION); - - // credits infornation - credits = title + " " + - _L("is based on Slic3r by Alessandro Ranellucci and the RepRap community.") + "\n" + - _L("Developed by Prusa Research.") + "\n\n" + - title + " " + _L("is licensed under the") + " " + _L("GNU Affero General Public License, version 3") + ".\n\n" + - _L("Contributions by Vojtech Bubnik, Enrico Turri, Oleksandra Iushchenko, Tamas Meszaros, Lukas Matena, Vojtech Kral, David Kocik and numerous others.") + "\n\n" + - _L("Artwork model by Leslie Ing"); - - title_font = version_font = credits_font = init_font; - } - } - m_constant_text; - - void init_constant_text() - { - m_constant_text.init(get_default_font(this)); - - // As default we use a system font for current display. - // Scale fonts in respect to banner width - - int text_banner_width = lround(0.4 * m_main_bitmap.GetWidth()) - roundl(m_scale * 50); // banner_width - margins - - float title_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.title).GetX(); - scale_font(m_constant_text.title_font, title_font_scale > 3.5f ? 3.5f : title_font_scale); - - float version_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.version).GetX(); - scale_font(m_constant_text.version_font, version_font_scale > 2.f ? 2.f : version_font_scale); - - // The width of the credits information string doesn't respect to the banner width some times. - // So, scale credits_font in the respect to the longest string width - int longest_string_width = word_wrap_string(m_constant_text.credits); - float font_scale = (float)text_banner_width / longest_string_width; - scale_font(m_constant_text.credits_font, font_scale); - } - - void set_bitmap(wxBitmap& bmp) - { - m_window->SetBitmap(bmp); - m_window->Refresh(); - m_window->Update(); - } - - void scale_bitmap(wxBitmap& bmp, float scale) - { - if (scale == 1.0) - return; - - wxImage image = bmp.ConvertToImage(); - if (!image.IsOk() || image.GetWidth() == 0 || image.GetHeight() == 0) - return; - - int width = int(scale * image.GetWidth()); - int height = int(scale * image.GetHeight()); - image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); - - bmp = wxBitmap(std::move(image)); - } - - void scale_font(wxFont& font, float scale) - { -#ifdef __WXMSW__ - // Workaround for the font scaling in respect to the current active display, - // not for the primary display, as it's implemented in Font.cpp - // See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/font.cpp - // void wxNativeFontInfo::SetFractionalPointSize(float pointSizeNew) - wxNativeFontInfo nfi= *font.GetNativeFontInfo(); - float pointSizeNew = wxDisplay(this).GetScaleFactor() * scale * font.GetPointSize(); - nfi.lf.lfHeight = nfi.GetLogFontHeightAtPPI(pointSizeNew, get_dpi_for_window(this)); - nfi.pointSize = pointSizeNew; - font = wxFont(nfi); -#else - font.Scale(scale); -#endif //__WXMSW__ - } - - // wrap a string for the strings no longer then 55 symbols - // return extent of the longest string - int word_wrap_string(wxString& input) - { - size_t line_len = 55;// count of symbols in one line - int idx = -1; - size_t cur_len = 0; - - wxString longest_sub_string; - auto get_longest_sub_string = [input](wxString &longest_sub_str, size_t cur_len, size_t i) { - if (cur_len > longest_sub_str.Len()) - longest_sub_str = input.SubString(i - cur_len + 1, i); - }; - - for (size_t i = 0; i < input.Len(); i++) - { - cur_len++; - if (input[i] == ' ') - idx = i; - if (input[i] == '\n') - { - get_longest_sub_string(longest_sub_string, cur_len, i); - idx = -1; - cur_len = 0; - } - if (cur_len >= line_len && idx >= 0) - { - get_longest_sub_string(longest_sub_string, cur_len, i); - input[idx] = '\n'; - cur_len = i - static_cast(idx); - } - } - - return GetTextExtent(longest_sub_string).GetX(); - } -}; - - -#ifdef __linux__ -bool static check_old_linux_datadir(const wxString& app_name) { - // If we are on Linux and the datadir does not exist yet, look into the old - // location where the datadir was before version 2.3. If we find it there, - // tell the user that he might wanna migrate to the new location. - // (https://github.com/prusa3d/PrusaSlicer/issues/2911) - // To be precise, the datadir should exist, it is created when single instance - // lock happens. Instead of checking for existence, check the contents. - - namespace fs = boost::filesystem; - - std::string new_path = Slic3r::data_dir(); - - wxString dir; - if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) - dir = wxFileName::GetHomeDir() + wxS("/.config"); - std::string default_path = (dir + "/" + app_name).ToUTF8().data(); - - if (new_path != default_path) { - // This happens when the user specifies a custom --datadir. - // Do not show anything in that case. - return true; - } - - fs::path data_dir = fs::path(new_path); - if (! fs::is_directory(data_dir)) - return true; // This should not happen. - - int file_count = std::distance(fs::directory_iterator(data_dir), fs::directory_iterator()); - - if (file_count <= 1) { // just cache dir with an instance lock - std::string old_path = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); - - if (fs::is_directory(old_path)) { - wxString msg = from_u8((boost::format(_u8L("Starting with %1% 2.3, configuration " - "directory on Linux has changed (according to XDG Base Directory Specification) to \n%2%.\n\n" - "This directory did not exist yet (maybe you run the new version for the first time).\nHowever, " - "an old %1% configuration directory was detected in \n%3%.\n\n" - "Consider moving the contents of the old directory to the new location in order to access " - "your profiles, etc.\nNote that if you decide to downgrade %1% in future, it will use the old " - "location again.\n\n" - "What do you want to do now?")) % SLIC3R_APP_NAME % new_path % old_path).str()); - wxString caption = from_u8((boost::format(_u8L("%s - BREAKING CHANGE")) % SLIC3R_APP_NAME).str()); - RichMessageDialog dlg(nullptr, msg, caption, wxYES_NO); - dlg.SetYesNoLabels(_L("Quit, I will move my data now"), _L("Start the application")); - if (dlg.ShowModal() != wxID_NO) - return false; - } - } else { - // If the new directory exists, be silent. The user likely already saw the message. - } - return true; -} -#endif - -#ifdef _WIN32 -#if 0 // External Updater is replaced with AppUpdater.cpp -static bool run_updater_win() -{ - // find updater exe - boost::filesystem::path path_updater = boost::dll::program_location().parent_path() / "prusaslicer-updater.exe"; - // run updater. Original args: /silent -restartapp prusa-slicer.exe -startappfirst - std::string msg; - bool res = create_process(path_updater, L"/silent", msg); - if (!res) - BOOST_LOG_TRIVIAL(error) << msg; - return res; -} -#endif // 0 -#endif // _WIN32 - -struct FileWildcards { - std::string_view title; - std::vector file_extensions; -}; - - - -static const FileWildcards file_wildcards_by_type[FT_SIZE] = { - /* FT_STL */ { "STL files"sv, { ".stl"sv } }, - /* FT_OBJ */ { "OBJ files"sv, { ".obj"sv } }, - /* FT_OBJECT */ { "Object files"sv, { ".stl"sv, ".obj"sv } }, - /* FT_STEP */ { "STEP files"sv, { ".stp"sv, ".step"sv } }, - /* FT_AMF */ { "AMF files"sv, { ".amf"sv, ".zip.amf"sv, ".xml"sv } }, - /* FT_3MF */ { "3MF files"sv, { ".3mf"sv } }, - /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv, ".gco"sv, ".g"sv, ".ngc"sv } }, - /* FT_MODEL */ { "Known files"sv, { ".stl"sv, ".obj"sv, ".3mf"sv, ".amf"sv, ".zip.amf"sv, ".xml"sv, ".step"sv, ".stp"sv } }, - /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv, ".amf"sv, ".zip.amf"sv } }, - /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, - - /* FT_INI */ { "INI files"sv, { ".ini"sv } }, - /* FT_SVG */ { "SVG files"sv, { ".svg"sv } }, - - /* FT_TEX */ { "Texture"sv, { ".png"sv, ".svg"sv } }, - - /* FT_SL1 */ { "Masked SLA files"sv, { ".sl1"sv, ".sl1s"sv, ".pwmx"sv } }, -}; - -#if ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR -wxString file_wildcards(FileType file_type) -{ - const FileWildcards& data = file_wildcards_by_type[file_type]; - std::string title; - std::string mask; - - // Generate cumulative first item - for (const std::string_view& ext : data.file_extensions) { - if (title.empty()) { - title = "*"; - title += ext; - mask = title; - } - else { - title += ", *"; - title += ext; - mask += ";*"; - mask += ext; - } - mask += ";*"; - mask += boost::to_upper_copy(std::string(ext)); - } - - wxString ret = GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); - - // Adds an item for each of the extensions - if (data.file_extensions.size() > 1) { - for (const std::string_view& ext : data.file_extensions) { - title = "*"; - title += ext; - ret += GUI::format_wxstr("|%s (%s)|%s", data.title, title, title); - } - } - - return ret; -} -#else -// This function produces a Win32 file dialog file template mask to be consumed by wxWidgets on all platforms. -// The function accepts a custom extension parameter. If the parameter is provided, the custom extension -// will be added as a fist to the list. This is important for a "file save" dialog on OSX, which strips -// an extension from the provided initial file name and substitutes it with the default extension (the first one in the template). -wxString file_wildcards(FileType file_type, const std::string &custom_extension) -{ - const FileWildcards& data = file_wildcards_by_type[file_type]; - std::string title; - std::string mask; - std::string custom_ext_lower; - - if (! custom_extension.empty()) { - // Generate an extension into the title mask and into the list of extensions. - custom_ext_lower = boost::to_lower_copy(custom_extension); - const std::string custom_ext_upper = boost::to_upper_copy(custom_extension); - if (custom_ext_lower == custom_extension) { - // Add a lower case version. - title = std::string("*") + custom_ext_lower; - mask = title; - // Add an upper case version. - mask += ";*"; - mask += custom_ext_upper; - } else if (custom_ext_upper == custom_extension) { - // Add an upper case version. - title = std::string("*") + custom_ext_upper; - mask = title; - // Add a lower case version. - mask += ";*"; - mask += custom_ext_lower; - } else { - // Add the mixed case version only. - title = std::string("*") + custom_extension; - mask = title; - } - } - - for (const std::string_view &ext : data.file_extensions) - // Only add an extension if it was not added first as the custom extension. - if (ext != custom_ext_lower) { - if (title.empty()) { - title = "*"; - title += ext; - mask = title; - } else { - title += ", *"; - title += ext; - mask += ";*"; - mask += ext; - } - mask += ";*"; - mask += boost::to_upper_copy(std::string(ext)); - } - - return GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); -} -#endif // ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR - -static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); } - -#ifdef WIN32 -#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) -static void register_win32_dpi_event() -{ - enum { WM_DPICHANGED_ = 0x02e0 }; - - wxWindow::MSWRegisterMessageHandler(WM_DPICHANGED_, [](wxWindow *win, WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) { - const int dpi = wParam & 0xffff; - const auto rect = reinterpret_cast(lParam); - const wxRect wxrect(wxPoint(rect->top, rect->left), wxPoint(rect->bottom, rect->right)); - - DpiChangedEvent evt(EVT_DPI_CHANGED_SLICER, dpi, wxrect); - win->GetEventHandler()->AddPendingEvent(evt); - - return true; - }); -} -#endif // !wxVERSION_EQUAL_OR_GREATER_THAN - -static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; - -static void register_win32_device_notification_event() -{ - wxWindow::MSWRegisterMessageHandler(WM_DEVICECHANGE, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. - auto main_frame = dynamic_cast(win); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); - if (plater == nullptr) - // Maybe some other top level window like a dialog or maybe a pop-up menu? - return true; - PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; - switch (wParam) { - case DBT_DEVICEARRIVAL: - if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) - plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); - else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { - PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; -// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) { -// printf("DBT_DEVICEARRIVAL %d - Media has arrived: %ws\n", msg_count, lpdbi->dbcc_name); - if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) - plater->GetEventHandler()->AddPendingEvent(HIDDeviceAttachedEvent(EVT_HID_DEVICE_ATTACHED, boost::nowide::narrow(lpdbi->dbcc_name))); - } - break; - case DBT_DEVICEREMOVECOMPLETE: - if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) - plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); - else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { - PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; -// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) -// printf("DBT_DEVICEARRIVAL %d - Media was removed: %ws\n", msg_count, lpdbi->dbcc_name); - if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) - plater->GetEventHandler()->AddPendingEvent(HIDDeviceDetachedEvent(EVT_HID_DEVICE_DETACHED, boost::nowide::narrow(lpdbi->dbcc_name))); - } - break; - default: - break; - } - return true; - }); - - wxWindow::MSWRegisterMessageHandler(MainFrame::WM_USER_MEDIACHANGED, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. - auto main_frame = dynamic_cast(win); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); - if (plater == nullptr) - // Maybe some other top level window like a dialog or maybe a pop-up menu? - return true; - wchar_t sPath[MAX_PATH]; - if (lParam == SHCNE_MEDIAINSERTED || lParam == SHCNE_MEDIAREMOVED) { - struct _ITEMIDLIST* pidl = *reinterpret_cast(wParam); - if (! SHGetPathFromIDList(pidl, sPath)) { - BOOST_LOG_TRIVIAL(error) << "MediaInserted: SHGetPathFromIDList failed"; - return false; - } - } - switch (lParam) { - case SHCNE_MEDIAINSERTED: - { - //printf("SHCNE_MEDIAINSERTED %S\n", sPath); - plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); - break; - } - case SHCNE_MEDIAREMOVED: - { - //printf("SHCNE_MEDIAREMOVED %S\n", sPath); - plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); - break; - } - default: -// printf("Unknown\n"); - break; - } - return true; - }); - - wxWindow::MSWRegisterMessageHandler(WM_INPUT, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - auto main_frame = dynamic_cast(Slic3r::GUI::find_toplevel_parent(win)); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); -// if (wParam == RIM_INPUTSINK && plater != nullptr && main_frame->IsActive()) { - if (wParam == RIM_INPUT && plater != nullptr && main_frame->IsActive()) { - RAWINPUT raw; - UINT rawSize = sizeof(RAWINPUT); - ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &raw, &rawSize, sizeof(RAWINPUTHEADER)); - if (raw.header.dwType == RIM_TYPEHID && plater->get_mouse3d_controller().handle_raw_input_win32(raw.data.hid.bRawData, raw.data.hid.dwSizeHid)) - return true; - } - return false; - }); - - wxWindow::MSWRegisterMessageHandler(WM_COPYDATA, [](wxWindow* win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - COPYDATASTRUCT* copy_data_structure = { 0 }; - copy_data_structure = (COPYDATASTRUCT*)lParam; - if (copy_data_structure->dwData == 1) { - LPCWSTR arguments = (LPCWSTR)copy_data_structure->lpData; - Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(boost::nowide::narrow(arguments)); - } - return true; - }); -} -#endif // WIN32 - -static void generic_exception_handle() -{ - // Note: Some wxWidgets APIs use wxLogError() to report errors, eg. wxImage - // - see https://docs.wxwidgets.org/3.1/classwx_image.html#aa249e657259fe6518d68a5208b9043d0 - // - // wxLogError typically goes around exception handling and display an error dialog some time - // after an error is logged even if exception handling and OnExceptionInMainLoop() take place. - // This is why we use wxLogError() here as well instead of a custom dialog, because it accumulates - // errors if multiple have been collected and displays just one error message for all of them. - // Otherwise we would get multiple error messages for one missing png, for example. - // - // If a custom error message window (or some other solution) were to be used, it would be necessary - // to turn off wxLogError() usage in wx APIs, most notably in wxImage - // - see https://docs.wxwidgets.org/trunk/classwx_image.html#aa32e5d3507cc0f8c3330135bc0befc6a - - try { - throw; - } catch (const std::bad_alloc& ex) { - // bad_alloc in main thread is most likely fatal. Report immediately to the user (wxLogError would be delayed) - // and terminate the app so it is at least certain to happen now. - wxString errmsg = wxString::Format(_L("%s has encountered an error. It was likely caused by running out of memory. " - "If you are sure you have enough RAM on your system, this may also be a bug and we would " - "be glad if you reported it.\n\nThe application will now terminate."), SLIC3R_APP_NAME); - wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Fatal error"), wxOK | wxICON_ERROR); - BOOST_LOG_TRIVIAL(error) << boost::format("std::bad_alloc exception: %1%") % ex.what(); - std::terminate(); - } catch (const boost::io::bad_format_string& ex) { - wxString errmsg = _L("PrusaSlicer has encountered a localization error. " - "Please report to PrusaSlicer team, what language was active and in which scenario " - "this issue happened. Thank you.\n\nThe application will now terminate."); - wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Critical error"), wxOK | wxICON_ERROR); - BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); - std::terminate(); - throw; - } catch (const std::exception& ex) { - wxLogError(format_wxstr(_L("Internal error: %1%"), ex.what())); - BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); - throw; - } -} - -void GUI_App::post_init() -{ - assert(initialized()); - if (! this->initialized()) - throw Slic3r::RuntimeError("Calling post_init() while not yet initialized"); - - if (this->is_gcode_viewer()) { - if (! this->init_params->input_files.empty()) - this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str())); - } - else { - if (! this->init_params->preset_substitutions.empty()) - show_substitutions_info(this->init_params->preset_substitutions); - -#if 0 - // Load the cummulative config over the currently active profiles. - //FIXME if multiple configs are loaded, only the last one will have an effect. - // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). - // As of now only the full configs are supported here. - if (!m_print_config.empty()) - this->gui->mainframe->load_config(m_print_config); -#endif - if (! this->init_params->load_configs.empty()) - // Load the last config to give it a name at the UI. The name of the preset may be later - // changed by loading an AMF or 3MF. - //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. - this->mainframe->load_config_file(this->init_params->load_configs.back()); - // If loading a 3MF file, the config is loaded from the last one. - if (!this->init_params->input_files.empty()) { - const std::vector res = this->plater()->load_files(this->init_params->input_files, true, true); - if (!res.empty() && this->init_params->input_files.size() == 1) { - // Update application titlebar when opening a project file - const std::string& filename = this->init_params->input_files.front(); - if (boost::algorithm::iends_with(filename, ".amf") || - boost::algorithm::iends_with(filename, ".amf.xml") || - boost::algorithm::iends_with(filename, ".3mf")) - this->plater()->set_project_filename(from_u8(filename)); - } - } - if (! this->init_params->extra_config.empty()) - this->mainframe->load_config(this->init_params->extra_config); - } - - // show "Did you know" notification - if (app_config->get("show_hints") == "1" && ! is_gcode_viewer()) - plater_->get_notification_manager()->push_hint_notification(true); - - // The extra CallAfter() is needed because of Mac, where this is the only way - // to popup a modal dialog on start without screwing combo boxes. - // This is ugly but I honestly found no better way to do it. - // Neither wxShowEvent nor wxWindowCreateEvent work reliably. - if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. - if (! this->check_updates(false)) - // Configuration is not compatible and reconfigure was refused by the user. Application is closing. - return; - CallAfter([this] { - bool cw_showed = this->config_wizard_startup(); - this->preset_updater->sync(preset_bundle); - this->app_version_check(false); - if (! cw_showed) { - // The CallAfter is needed as well, without it, GL extensions did not show. - // Also, we only want to show this when the wizard does not, so the new user - // sees something else than "we want something" on the first start. - show_send_system_info_dialog_if_needed(); - } - }); - } - - // Set PrusaSlicer version and save to PrusaSlicer.ini or PrusaSlicerGcodeViewer.ini. - app_config->set("version", SLIC3R_VERSION); - app_config->save(); - -#ifdef _WIN32 - // Sets window property to mainframe so other instances can indentify it. - OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); -#endif //WIN32 -} - -IMPLEMENT_APP(GUI_App) - -GUI_App::GUI_App(EAppMode mode) - : wxApp() - , m_app_mode(mode) - , m_em_unit(10) - , m_imgui(new ImGuiWrapper()) - , m_removable_drive_manager(std::make_unique()) - , m_other_instance_message_handler(std::make_unique()) -{ - //app config initializes early becasuse it is used in instance checking in PrusaSlicer.cpp - this->init_app_config(); - // init app downloader after path to datadir is set - m_app_updater = std::make_unique(); -} - -GUI_App::~GUI_App() -{ - if (app_config != nullptr) - delete app_config; - - if (preset_bundle != nullptr) - delete preset_bundle; - - if (preset_updater != nullptr) - delete preset_updater; -} - -// If formatted for github, plaintext with OpenGL extensions enclosed into
. -// Otherwise HTML formatted for the system info dialog. -std::string GUI_App::get_gl_info(bool for_github) -{ - return OpenGLManager::get_gl_info().to_string(for_github); -} - -wxGLContext* GUI_App::init_glcontext(wxGLCanvas& canvas) -{ -#if ENABLE_GL_CORE_PROFILE -#if ENABLE_OPENGL_DEBUG_OPTION - return m_opengl_mgr.init_glcontext(canvas, init_params != nullptr ? init_params->opengl_version : std::make_pair(0, 0), - init_params != nullptr ? init_params->opengl_debug : false); -#else - return m_opengl_mgr.init_glcontext(canvas, init_params != nullptr ? init_params->opengl_version : std::make_pair(0, 0)); -#endif // ENABLE_OPENGL_DEBUG_OPTION -#else - return m_opengl_mgr.init_glcontext(canvas); -#endif // ENABLE_GL_CORE_PROFILE -} - -bool GUI_App::init_opengl() -{ -#ifdef __linux__ - bool status = m_opengl_mgr.init_gl(); - m_opengl_initialized = true; - return status; -#else - return m_opengl_mgr.init_gl(); -#endif -} - -// gets path to PrusaSlicer.ini, returns semver from first line comment -static boost::optional parse_semver_from_ini(std::string path) -{ - std::ifstream stream(path); - std::stringstream buffer; - buffer << stream.rdbuf(); - std::string body = buffer.str(); - size_t start = body.find("PrusaSlicer "); - if (start == std::string::npos) - return boost::none; - body = body.substr(start + 12); - size_t end = body.find_first_of(" \n"); - if (end < body.size()) - body.resize(end); - return Semver::parse(body); -} - -void GUI_App::init_app_config() -{ - // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. - -// SetAppName(SLIC3R_APP_KEY); - SetAppName(SLIC3R_APP_KEY "-alpha"); -// SetAppName(SLIC3R_APP_KEY "-beta"); - - -// SetAppDisplayName(SLIC3R_APP_NAME); - - // Set the Slic3r data directory at the Slic3r XS module. - // Unix: ~/ .Slic3r - // Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r" - // Mac : "~/Library/Application Support/Slic3r" - - if (data_dir().empty()) { - #ifndef __linux__ - set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()); - #else - // Since version 2.3, config dir on Linux is in ${XDG_CONFIG_HOME}. - // https://github.com/prusa3d/PrusaSlicer/issues/2911 - wxString dir; - if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) - dir = wxFileName::GetHomeDir() + wxS("/.config"); - set_data_dir((dir + "/" + GetAppName()).ToUTF8().data()); - #endif - } else { - m_datadir_redefined = true; - } - - if (!app_config) - app_config = new AppConfig(is_editor() ? AppConfig::EAppMode::Editor : AppConfig::EAppMode::GCodeViewer); - - // load settings - m_app_conf_exists = app_config->exists(); - if (m_app_conf_exists) { - std::string error = app_config->load(); - if (!error.empty()) { - // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - if (is_editor()) { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - else { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - } - } -} - -// returns old config path to copy from if such exists, -// returns an empty string if such config path does not exists or if it cannot be loaded. -std::string GUI_App::check_older_app_config(Semver current_version, bool backup) -{ - std::string older_data_dir_path; - - // If the config folder is redefined - do not check - if (m_datadir_redefined) - return {}; - - // find other version app config (alpha / beta / release) - std::string config_path = app_config->config_path(); - boost::filesystem::path parent_file_path(config_path); - std::string filename = parent_file_path.filename().string(); - parent_file_path.remove_filename().remove_filename(); - - std::vector candidates; - - if (SLIC3R_APP_KEY "-alpha" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-alpha" / filename); - if (SLIC3R_APP_KEY "-beta" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-beta" / filename); - if (SLIC3R_APP_KEY != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY / filename); - - Semver last_semver = current_version; - for (const auto& candidate : candidates) { - if (boost::filesystem::exists(candidate)) { - // parse - boost::optionalother_semver = parse_semver_from_ini(candidate.string()); - if (other_semver && *other_semver > last_semver) { - last_semver = *other_semver; - older_data_dir_path = candidate.parent_path().string(); - } - } - } - if (older_data_dir_path.empty()) - return {}; - BOOST_LOG_TRIVIAL(info) << "last app config file used: " << older_data_dir_path; - // ask about using older data folder - - InfoDialog msg(nullptr - , format_wxstr(_L("You are opening %1% version %2%."), SLIC3R_APP_NAME, SLIC3R_VERSION) - , backup ? - format_wxstr(_L( - "The active configuration was created by %1% %2%," - "\nwhile a newer configuration was found in %3%" - "\ncreated by %1% %4%." - "\n\nShall the newer configuration be imported?" - "\nIf so, your active configuration will be backed up before importing the new configuration." - ) - , SLIC3R_APP_NAME, current_version.to_string(), older_data_dir_path, last_semver.to_string()) - : format_wxstr(_L( - "An existing configuration was found in %3%" - "\ncreated by %1% %2%." - "\n\nShall this configuration be imported?" - ) - , SLIC3R_APP_NAME, last_semver.to_string(), older_data_dir_path) - , true, wxYES_NO); - - if (backup) { - msg.SetButtonLabel(wxID_YES, _L("Import")); - msg.SetButtonLabel(wxID_NO, _L("Don't import")); - } - - if (msg.ShowModal() == wxID_YES) { - std::string snapshot_id; - if (backup) { - const Config::Snapshot* snapshot{ nullptr }; - if (! GUI::Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_USER, "", - _u8L("Continue and import newer configuration?"), &snapshot)) - return {}; - if (snapshot) { - // Save snapshot ID before loading the alternate AppConfig, as loading the alternate AppConfig may fail. - snapshot_id = snapshot->id; - assert(! snapshot_id.empty()); - app_config->set("on_snapshot", snapshot_id); - } else - BOOST_LOG_TRIVIAL(error) << "Failed to take congiguration snapshot"; - } - - // load app config from older file - std::string error = app_config->load((boost::filesystem::path(older_data_dir_path) / filename).string()); - if (!error.empty()) { - // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - if (is_editor()) { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - else { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - } - if (!snapshot_id.empty()) - app_config->set("on_snapshot", snapshot_id); - m_app_conf_exists = true; - return older_data_dir_path; - } - return {}; -} - -void GUI_App::init_single_instance_checker(const std::string &name, const std::string &path) -{ - BOOST_LOG_TRIVIAL(debug) << "init wx instance checker " << name << " "<< path; - m_single_instance_checker = std::make_unique(boost::nowide::widen(name), boost::nowide::widen(path)); -} - -bool GUI_App::OnInit() -{ - try { - return on_init_inner(); - } catch (const std::exception&) { - generic_exception_handle(); - return false; - } -} - -bool GUI_App::on_init_inner() -{ - // Set initialization of image handlers before any UI actions - See GH issue #7469 - wxInitAllImageHandlers(); - -#if defined(_WIN32) && ! defined(_WIN64) - // Win32 32bit build. - if (wxPlatformInfo::Get().GetArchName().substr(0, 2) == "64") { - RichMessageDialog dlg(nullptr, - _L("You are running a 32 bit build of PrusaSlicer on 64-bit Windows." - "\n32 bit build of PrusaSlicer will likely not be able to utilize all the RAM available in the system." - "\nPlease download and install a 64 bit build of PrusaSlicer from https://www.prusa3d.cz/prusaslicer/." - "\nDo you wish to continue?"), - "PrusaSlicer", wxICON_QUESTION | wxYES_NO); - if (dlg.ShowModal() != wxID_YES) - return false; - } -#endif // _WIN64 - - // Forcing back menu icons under gtk2 and gtk3. Solution is based on: - // https://docs.gtk.org/gtk3/class.Settings.html - // see also https://docs.wxwidgets.org/3.0/classwx_menu_item.html#a2b5d6bcb820b992b1e4709facbf6d4fb - // TODO: Find workaround for GTK4 -#if defined(__WXGTK20__) || defined(__WXGTK3__) - g_object_set (gtk_settings_get_default (), "gtk-menu-images", TRUE, NULL); -#endif - - // Verify resources path - const wxString resources_dir = from_u8(Slic3r::resources_dir()); - wxCHECK_MSG(wxDirExists(resources_dir), false, - wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); - -#ifdef __linux__ - if (! check_old_linux_datadir(GetAppName())) { - std::cerr << "Quitting, user chose to move their data to new location." << std::endl; - return false; - } -#endif - - // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. -// wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0); - // Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible - // performance when working on high resolution multi-display setups. -// wxSystemOptions::SetOption("msw.notebook.themed-background", 0); - -// Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION; - - // !!! Initialization of UI settings as a language, application color mode, fonts... have to be done before first UI action. - // Like here, before the show InfoDialog in check_older_app_config() - - // If load_language() fails, the application closes. - load_language(wxString(), true); -#ifdef _MSW_DARK_MODE - bool init_dark_color_mode = app_config->get("dark_color_mode") == "1"; - bool init_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; - NppDarkMode::InitDarkMode(init_dark_color_mode, init_sys_menu_enabled); -#endif - // initialize label colors and fonts - init_label_colours(); - init_fonts(); - - std::string older_data_dir_path; - if (m_app_conf_exists) { - if (app_config->orig_version().valid() && app_config->orig_version() < *Semver::parse(SLIC3R_VERSION)) - // Only copying configuration if it was saved with a newer slicer than the one currently running. - older_data_dir_path = check_older_app_config(app_config->orig_version(), true); - } else { - // No AppConfig exists, fresh install. Always try to copy from an alternate location, don't make backup of the current configuration. - older_data_dir_path = check_older_app_config(Semver(), false); - } - -#ifdef _MSW_DARK_MODE - // app_config can be updated in check_older_app_config(), so check if dark_color_mode and sys_menu_enabled was changed - if (bool new_dark_color_mode = app_config->get("dark_color_mode") == "1"; - init_dark_color_mode != new_dark_color_mode) { - NppDarkMode::SetDarkMode(new_dark_color_mode); - init_label_colours(); - update_label_colours_from_appconfig(); - } - if (bool new_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; - init_sys_menu_enabled != new_sys_menu_enabled) - NppDarkMode::SetSystemMenuForApp(new_sys_menu_enabled); -#endif - - if (is_editor()) { - std::string msg = Http::tls_global_init(); - std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location"); - bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes" && ssl_cert_store == Http::tls_system_cert_store(); - - if (!msg.empty() && !ssl_accept) { - RichMessageDialog - dlg(nullptr, - wxString::Format(_L("%s\nDo you want to continue?"), msg), - "PrusaSlicer", wxICON_QUESTION | wxYES_NO); - dlg.ShowCheckBox(_L("Remember my choice")); - if (dlg.ShowModal() != wxID_YES) return false; - - app_config->set("tls_cert_store_accepted", - dlg.IsCheckBoxChecked() ? "yes" : "no"); - app_config->set("tls_accepted_cert_store_location", - dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : ""); - } - } - - SplashScreen* scrn = nullptr; - if (app_config->get("show_splash_screen") == "1") { - // make a bitmap with dark grey banner on the left side - wxBitmap bmp = SplashScreen::MakeBitmap(wxBitmap(from_u8(var(is_editor() ? "splashscreen.jpg" : "splashscreen-gcodepreview.jpg")), wxBITMAP_TYPE_JPEG)); - - // Detect position (display) to show the splash screen - // Now this position is equal to the mainframe position - wxPoint splashscreen_pos = wxDefaultPosition; - bool default_splashscreen_pos = true; - if (app_config->has("window_mainframe") && app_config->get("restore_win_position") == "1") { - auto metrics = WindowMetrics::deserialize(app_config->get("window_mainframe")); - default_splashscreen_pos = metrics == boost::none; - if (!default_splashscreen_pos) - splashscreen_pos = metrics->get_rect().GetPosition(); - } - - if (!default_splashscreen_pos) { - // workaround for crash related to the positioning of the window on secondary monitor - get_app_config()->set("restore_win_position", "crashed_at_splashscreen_pos"); - get_app_config()->save(); - } - - // create splash screen with updated bmp - scrn = new SplashScreen(bmp.IsOk() ? bmp : get_bmp_bundle("PrusaSlicer", 400)->GetPreferredBitmapSizeAtScale(1.0), - wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_TIMEOUT, 4000, splashscreen_pos); - - if (!default_splashscreen_pos) - // revert "restore_win_position" value if application wasn't crashed - get_app_config()->set("restore_win_position", "1"); -#ifndef __linux__ - wxYield(); -#endif - scrn->SetText(_L("Loading configuration")+ dots); - } - - preset_bundle = new PresetBundle(); - - // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory - // supplied as argument to --datadir; in that case we should still run the wizard - preset_bundle->setup_directories(); - - if (! older_data_dir_path.empty()) { - preset_bundle->import_newer_configs(older_data_dir_path); - app_config->save(); - } - - if (is_editor()) { -#ifdef __WXMSW__ - if (app_config->get("associate_3mf") == "1") - associate_3mf_files(); - if (app_config->get("associate_stl") == "1") - associate_stl_files(); -#endif // __WXMSW__ - - preset_updater = new PresetUpdater(); - Bind(EVT_SLIC3R_VERSION_ONLINE, &GUI_App::on_version_read, this); - Bind(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, [this](const wxCommandEvent& evt) { - if (this->plater_ != nullptr && app_config->get("notify_release") == "all") { - std::string evt_string = into_u8(evt.GetString()); - if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(evt_string)) { - auto notif_type = (evt_string.find("beta") != std::string::npos ? NotificationType::NewBetaAvailable : NotificationType::NewAlphaAvailable); - this->plater_->get_notification_manager()->push_notification( notif_type - , NotificationManager::NotificationLevel::ImportantNotificationLevel - , Slic3r::format(_u8L("New prerelease version %1% is available."), evt_string) - , _u8L("See Releases page.") - , [](wxEvtHandler* evnthndlr) {wxGetApp().open_browser_with_warning_dialog("https://github.com/prusa3d/PrusaSlicer/releases"); return true; } - ); - } - } - }); - Bind(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, [this](const wxCommandEvent& evt) { - //lm:This does not force a render. The progress bar only updateswhen the mouse is moved. - if (this->plater_ != nullptr) - this->plater_->get_notification_manager()->set_download_progress_percentage((float)std::stoi(into_u8(evt.GetString())) / 100.f ); - }); - - Bind(EVT_SLIC3R_APP_DOWNLOAD_FAILED, [this](const wxCommandEvent& evt) { - if (this->plater_ != nullptr) - this->plater_->get_notification_manager()->close_notification_of_type(NotificationType::AppDownload); - if(!evt.GetString().IsEmpty()) - show_error(nullptr, evt.GetString()); - }); - - Bind(EVT_SLIC3R_APP_OPEN_FAILED, [](const wxCommandEvent& evt) { - show_error(nullptr, evt.GetString()); - }); - } - else { -#ifdef __WXMSW__ - if (app_config->get("associate_gcode") == "1") - associate_gcode_files(); -#endif // __WXMSW__ - } - - // Suppress the '- default -' presets. - preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1"); - try { - // Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only. - // If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force - // installation of a compatible system preset, thus nullifying the system preset substitutions. - init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); - } catch (const std::exception &ex) { - show_error(nullptr, ex.what()); - } - -#ifdef WIN32 -#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) - register_win32_dpi_event(); -#endif // !wxVERSION_EQUAL_OR_GREATER_THAN - register_win32_device_notification_event(); -#endif // WIN32 - - // Let the libslic3r know the callback, which will translate messages on demand. - Slic3r::I18N::set_translate_callback(libslic3r_translate_callback); - - // application frame - if (scrn && is_editor()) - scrn->SetText(_L("Preparing settings tabs") + dots); - - mainframe = new MainFrame(); - // hide settings tabs after first Layout - if (is_editor()) - mainframe->select_tab(size_t(0)); - - sidebar().obj_list()->init_objects(); // propagate model objects to object list -// update_mode(); // !!! do that later - SetTopWindow(mainframe); - - plater_->init_notification_manager(); - - m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); - - if (is_gcode_viewer()) { - mainframe->update_layout(); - if (plater_ != nullptr) - // ensure the selected technology is ptFFF - plater_->set_printer_technology(ptFFF); - } - else - load_current_presets(); - - // Save the active profiles as a "saved into project". - update_saved_preset_from_current_preset(); - - if (plater_ != nullptr) { - // Save the names of active presets and project specific config into ProjectDirtyStateManager. - plater_->reset_project_dirty_initial_presets(); - // Update Project dirty state, update application title bar. - plater_->update_project_dirty_from_presets(); - } - - mainframe->Show(true); - - obj_list()->set_min_height(); - - update_mode(); // update view mode after fix of the object_list size - -#ifdef __APPLE__ - other_instance_message_handler()->bring_instance_forward(); -#endif //__APPLE__ - - Bind(wxEVT_IDLE, [this](wxIdleEvent& event) - { - if (! plater_) - return; - - this->obj_manipul()->update_if_dirty(); - - // An ugly solution to GH #5537 in which GUI_App::init_opengl (normally called from events wxEVT_PAINT - // and wxEVT_SET_FOCUS before GUI_App::post_init is called) wasn't called before GUI_App::post_init and OpenGL wasn't initialized. -#ifdef __linux__ - if (! m_post_initialized && m_opengl_initialized) { -#else - if (! m_post_initialized) { -#endif - m_post_initialized = true; -#ifdef WIN32 - this->mainframe->register_win32_callbacks(); -#endif - this->post_init(); - } - - if (m_post_initialized && app_config->dirty() && app_config->get("autosave") == "1") - app_config->save(); - }); - - m_initialized = true; - - if (const std::string& crash_reason = app_config->get("restore_win_position"); - boost::starts_with(crash_reason,"crashed")) - { - wxString preferences_item = _L("Restore window position on start"); - InfoDialog dialog(nullptr, - _L("PrusaSlicer started after a crash"), - format_wxstr(_L("PrusaSlicer crashed last time when attempting to set window position.\n" - "We are sorry for the inconvenience, it unfortunately happens with certain multiple-monitor setups.\n" - "More precise reason for the crash: \"%1%\".\n" - "For more information see our GitHub issue tracker: \"%2%\" and \"%3%\"\n\n" - "To avoid this problem, consider disabling \"%4%\" in \"Preferences\". " - "Otherwise, the application will most likely crash again next time."), - "" + from_u8(crash_reason) + "", - "#2939", - "#5573", - "" + preferences_item + ""), - true, wxYES_NO); - - dialog.SetButtonLabel(wxID_YES, format_wxstr(_L("Disable \"%1%\""), preferences_item)); - dialog.SetButtonLabel(wxID_NO, format_wxstr(_L("Leave \"%1%\" enabled") , preferences_item)); - - auto answer = dialog.ShowModal(); - if (answer == wxID_YES) - app_config->set("restore_win_position", "0"); - else if (answer == wxID_NO) - app_config->set("restore_win_position", "1"); - app_config->save(); - } - - return true; -} - -unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) -{ - double r = colour.Red(); - double g = colour.Green(); - double b = colour.Blue(); - - return std::round(std::sqrt( - r * r * .241 + - g * g * .691 + - b * b * .068 - )); -} - -bool GUI_App::dark_mode() -{ -#if __APPLE__ - // The check for dark mode returns false positive on 10.12 and 10.13, - // which allowed setting dark menu bar and dock area, which is - // is detected as dark mode. We must run on at least 10.14 where the - // proper dark mode was first introduced. - return wxPlatformInfo::Get().CheckOSVersion(10, 14) && mac_dark_mode(); -#else - if (wxGetApp().app_config->has("dark_color_mode")) - return wxGetApp().app_config->get("dark_color_mode") == "1"; - return check_dark_mode(); -#endif -} - -const wxColour GUI_App::get_label_default_clr_system() -{ - return dark_mode() ? wxColour(115, 220, 103) : wxColour(26, 132, 57); -} - -const wxColour GUI_App::get_label_default_clr_modified() -{ - return dark_mode() ? wxColour(253, 111, 40) : wxColour(252, 77, 1); -} - -void GUI_App::init_label_colours() -{ - m_color_label_modified = get_label_default_clr_modified(); - m_color_label_sys = get_label_default_clr_system(); - - bool is_dark_mode = dark_mode(); -#ifdef _WIN32 - m_color_label_default = is_dark_mode ? wxColour(250, 250, 250): wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); - m_color_highlight_label_default = is_dark_mode ? wxColour(230, 230, 230): wxSystemSettings::GetColour(/*wxSYS_COLOUR_HIGHLIGHTTEXT*/wxSYS_COLOUR_WINDOWTEXT); - m_color_highlight_default = is_dark_mode ? wxColour(78, 78, 78) : wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); - m_color_hovered_btn_label = is_dark_mode ? wxColour(253, 111, 40) : wxColour(252, 77, 1); - m_color_default_btn_label = is_dark_mode ? wxColour(255, 181, 100): wxColour(203, 61, 0); - m_color_selected_btn_bg = is_dark_mode ? wxColour(95, 73, 62) : wxColour(228, 220, 216); -#else - m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); -#endif - m_color_window_default = is_dark_mode ? wxColour(43, 43, 43) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); -} - -void GUI_App::update_label_colours_from_appconfig() -{ - if (app_config->has("label_clr_sys")) { - auto str = app_config->get("label_clr_sys"); - if (str != "") - m_color_label_sys = wxColour(str); - } - - if (app_config->has("label_clr_modified")) { - auto str = app_config->get("label_clr_modified"); - if (str != "") - m_color_label_modified = wxColour(str); - } -} - -void GUI_App::update_label_colours() -{ - for (Tab* tab : tabs_list) - tab->update_label_colours(); -} - -#ifdef _WIN32 -static bool is_focused(HWND hWnd) -{ - HWND hFocusedWnd = ::GetFocus(); - return hFocusedWnd && hWnd == hFocusedWnd; -} - -static bool is_default(wxWindow* win) -{ - wxTopLevelWindow* tlw = find_toplevel_parent(win); - if (!tlw) - return false; - - return win == tlw->GetDefaultItem(); -} -#endif - -void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool just_font/* = false*/) -{ -#ifdef _WIN32 - bool is_focused_button = false; - bool is_default_button = false; - if (wxButton* btn = dynamic_cast(window)) { - if (!(btn->GetWindowStyle() & wxNO_BORDER)) { - btn->SetWindowStyle(btn->GetWindowStyle() | wxNO_BORDER); - highlited = true; - } - // button marking - { - auto mark_button = [this, btn, highlited](const bool mark) { - if (btn->GetLabel().IsEmpty()) - btn->SetBackgroundColour(mark ? m_color_selected_btn_bg : highlited ? m_color_highlight_default : m_color_window_default); - else - btn->SetForegroundColour(mark ? m_color_hovered_btn_label : (is_default(btn) ? m_color_default_btn_label : m_color_label_default)); - btn->Refresh(); - btn->Update(); - }; - - // hovering - btn->Bind(wxEVT_ENTER_WINDOW, [mark_button](wxMouseEvent& event) { mark_button(true); event.Skip(); }); - btn->Bind(wxEVT_LEAVE_WINDOW, [mark_button, btn](wxMouseEvent& event) { mark_button(is_focused(btn->GetHWND())); event.Skip(); }); - // focusing - btn->Bind(wxEVT_SET_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(true); event.Skip(); }); - btn->Bind(wxEVT_KILL_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(false); event.Skip(); }); - - is_focused_button = is_focused(btn->GetHWND()); - is_default_button = is_default(btn); - if (is_focused_button || is_default_button) - mark_button(is_focused_button); - } - } - else if (wxTextCtrl* text = dynamic_cast(window)) { - if (text->GetBorder() != wxBORDER_SIMPLE) - text->SetWindowStyle(text->GetWindowStyle() | wxBORDER_SIMPLE); - } - else if (wxCheckListBox* list = dynamic_cast(window)) { - list->SetWindowStyle(list->GetWindowStyle() | wxBORDER_SIMPLE); - list->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - for (size_t i = 0; i < list->GetCount(); i++) - if (wxOwnerDrawn* item = list->GetItem(i)) { - item->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - item->SetTextColour(m_color_label_default); - } - return; - } - else if (dynamic_cast(window)) - window->SetWindowStyle(window->GetWindowStyle() | wxBORDER_SIMPLE); - - if (!just_font) - window->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - if (!is_focused_button && !is_default_button) - window->SetForegroundColour(m_color_label_default); -#endif -} - -// recursive function for scaling fonts for all controls in Window -#ifdef _WIN32 -static void update_dark_children_ui(wxWindow* window, bool just_buttons_update = false) -{ - bool is_btn = dynamic_cast(window) != nullptr; - if (!(just_buttons_update && !is_btn)) - wxGetApp().UpdateDarkUI(window, is_btn); - - auto children = window->GetChildren(); - for (auto child : children) { - update_dark_children_ui(child); - } -} -#endif - -// Note: Don't use this function for Dialog contains ScalableButtons -void GUI_App::UpdateDlgDarkUI(wxDialog* dlg, bool just_buttons_update/* = false*/) -{ -#ifdef _WIN32 - update_dark_children_ui(dlg, just_buttons_update); -#endif -} -void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) -{ -#ifdef _WIN32 - UpdateDarkUI(dvc, highlited ? dark_mode() : false); -#ifdef _MSW_DARK_MODE - dvc->RefreshHeaderDarkMode(&m_normal_font); -#endif //_MSW_DARK_MODE - if (dvc->HasFlag(wxDV_ROW_LINES)) - dvc->SetAlternateRowColour(m_color_highlight_default); - if (dvc->GetBorder() != wxBORDER_SIMPLE) - dvc->SetWindowStyle(dvc->GetWindowStyle() | wxBORDER_SIMPLE); -#endif -} - -void GUI_App::UpdateAllStaticTextDarkUI(wxWindow* parent) -{ -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(parent); - - auto children = parent->GetChildren(); - for (auto child : children) { - if (dynamic_cast(child)) - child->SetForegroundColour(m_color_label_default); - } -#endif -} - -void GUI_App::init_fonts() -{ - m_small_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - m_bold_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold(); - m_normal_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - -#ifdef __WXMAC__ - m_small_font.SetPointSize(11); - m_bold_font.SetPointSize(13); -#endif /*__WXMAC__*/ - - // wxSYS_OEM_FIXED_FONT and wxSYS_ANSI_FIXED_FONT use the same as - // DEFAULT in wxGtk. Use the TELETYPE family as a work-around - m_code_font = wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE)); - m_code_font.SetPointSize(m_normal_font.GetPointSize()); -} - -void GUI_App::update_fonts(const MainFrame *main_frame) -{ - /* Only normal and bold fonts are used for an application rescale, - * because of under MSW small and normal fonts are the same. - * To avoid same rescaling twice, just fill this values - * from rescaled MainFrame - */ - if (main_frame == nullptr) - main_frame = this->mainframe; - m_normal_font = main_frame->normal_font(); - m_small_font = m_normal_font; - m_bold_font = main_frame->normal_font().Bold(); - m_link_font = m_bold_font.Underlined(); - m_em_unit = main_frame->em_unit(); - m_code_font.SetPointSize(m_normal_font.GetPointSize()); -} - -void GUI_App::set_label_clr_modified(const wxColour& clr) -{ - if (m_color_label_modified == clr) - return; - m_color_label_modified = clr; - const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); - app_config->set("label_clr_modified", str); - app_config->save(); -} - -void GUI_App::set_label_clr_sys(const wxColour& clr) -{ - if (m_color_label_sys == clr) - return; - m_color_label_sys = clr; - const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); - app_config->set("label_clr_sys", str); - app_config->save(); -} - -bool GUI_App::tabs_as_menu() const -{ - return app_config->get("tabs_as_menu") == "1"; // || dark_mode(); -} - -wxSize GUI_App::get_min_size() const -{ - return wxSize(76*m_em_unit, 49 * m_em_unit); -} - -float GUI_App::toolbar_icon_scale(const bool is_limited/* = false*/) const -{ -#ifdef __APPLE__ - const float icon_sc = 1.0f; // for Retina display will be used its own scale -#else - const float icon_sc = m_em_unit*0.1f; -#endif // __APPLE__ - - const std::string& use_val = app_config->get("use_custom_toolbar_size"); - const std::string& val = app_config->get("custom_toolbar_size"); - const std::string& auto_val = app_config->get("auto_toolbar_size"); - - if (val.empty() || auto_val.empty() || use_val.empty()) - return icon_sc; - - int int_val = use_val == "0" ? 100 : atoi(val.c_str()); - // correct value in respect to auto_toolbar_size - int_val = std::min(atoi(auto_val.c_str()), int_val); - - if (is_limited && int_val < 50) - int_val = 50; - - return 0.01f * int_val * icon_sc; -} - -void GUI_App::set_auto_toolbar_icon_scale(float scale) const -{ -#ifdef __APPLE__ - const float icon_sc = 1.0f; // for Retina display will be used its own scale -#else - const float icon_sc = m_em_unit * 0.1f; -#endif // __APPLE__ - - long int_val = std::min(int(std::lround(scale / icon_sc * 100)), 100); - std::string val = std::to_string(int_val); - - app_config->set("auto_toolbar_size", val); -} - -// check user printer_presets for the containing information about "Print Host upload" -void GUI_App::check_printer_presets() -{ - std::vector preset_names = PhysicalPrinter::presets_with_print_host_information(preset_bundle->printers); - if (preset_names.empty()) - return; - - wxString msg_text = _L("You have the following presets with saved options for \"Print Host upload\"") + ":"; - for (const std::string& preset_name : preset_names) - msg_text += "\n \"" + from_u8(preset_name) + "\","; - msg_text.RemoveLast(); - msg_text += "\n\n" + _L("But since this version of PrusaSlicer we don't show this information in Printer Settings anymore.\n" - "Settings will be available in physical printers settings.") + "\n\n" + - _L("By default new Printer devices will be named as \"Printer N\" during its creation.\n" - "Note: This name can be changed later from the physical printers settings"); - - //wxMessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); - MessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); - - preset_bundle->physical_printers.load_printers_from_presets(preset_bundle->printers); -} - -void GUI_App::recreate_GUI(const wxString& msg_name) -{ - m_is_recreating_gui = true; - - mainframe->shutdown(); - - wxProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); - dlg.Pulse(); - dlg.Update(10, _L("Recreating") + dots); - - MainFrame *old_main_frame = mainframe; - mainframe = new MainFrame(); - if (is_editor()) - // hide settings tabs after first Layout - mainframe->select_tab(size_t(0)); - // Propagate model objects to object list. - sidebar().obj_list()->init_objects(); - SetTopWindow(mainframe); - - dlg.Update(30, _L("Recreating") + dots); - old_main_frame->Destroy(); - - dlg.Update(80, _L("Loading of current presets") + dots); - m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); - load_current_presets(); - mainframe->Show(true); - - dlg.Update(90, _L("Loading of a mode view") + dots); - - obj_list()->set_min_height(); - update_mode(); - - // #ys_FIXME_delete_after_testing Do we still need this ? -// CallAfter([]() { -// // Run the config wizard, don't offer the "reset user profile" checkbox. -// config_wizard_startup(true); -// }); - - m_is_recreating_gui = false; -} - -void GUI_App::system_info() -{ - SysInfoDialog dlg; - dlg.ShowModal(); -} - -void GUI_App::keyboard_shortcuts() -{ - KBShortcutsDialog dlg; - dlg.ShowModal(); -} - -// static method accepting a wxWindow object as first parameter -bool GUI_App::catch_error(std::function cb, - // wxMessageDialog* message_dialog, - const std::string& err /*= ""*/) -{ - if (!err.empty()) { - if (cb) - cb(); - // if (message_dialog) - // message_dialog->(err, "Error", wxOK | wxICON_ERROR); - show_error(/*this*/nullptr, err); - return true; - } - return false; -} - -// static method accepting a wxWindow object as first parameter -void fatal_error(wxWindow* parent) -{ - show_error(parent, ""); - // exit 1; // #ys_FIXME -} - -#ifdef _WIN32 - -#ifdef _MSW_DARK_MODE -static void update_scrolls(wxWindow* window) -{ - wxWindowList::compatibility_iterator node = window->GetChildren().GetFirst(); - while (node) - { - wxWindow* win = node->GetData(); - if (dynamic_cast(win) || - dynamic_cast(win) || - dynamic_cast(win)) - NppDarkMode::SetDarkExplorerTheme(win->GetHWND()); - - update_scrolls(win); - node = node->GetNext(); - } -} -#endif //_MSW_DARK_MODE - - -#ifdef _MSW_DARK_MODE -void GUI_App::force_menu_update() -{ - NppDarkMode::SetSystemMenuForApp(app_config->get("sys_menu_enabled") == "1"); -} -#endif //_MSW_DARK_MODE - -void GUI_App::force_colors_update() -{ -#ifdef _MSW_DARK_MODE - NppDarkMode::SetDarkMode(app_config->get("dark_color_mode") == "1"); - if (WXHWND wxHWND = wxToolTip::GetToolTipCtrl()) - NppDarkMode::SetDarkExplorerTheme((HWND)wxHWND); - NppDarkMode::SetDarkTitleBar(mainframe->GetHWND()); - NppDarkMode::SetDarkTitleBar(mainframe->m_settings_dialog.GetHWND()); -#endif //_MSW_DARK_MODE - m_force_colors_update = true; -} -#endif //_WIN32 - -// Called after the Preferences dialog is closed and the program settings are saved. -// Update the UI based on the current preferences. -void GUI_App::update_ui_from_settings() -{ - update_label_colours(); -#ifdef _WIN32 - // Upadte UI colors before Update UI from settings - if (m_force_colors_update) { - m_force_colors_update = false; - mainframe->force_color_changed(); - mainframe->diff_dialog.force_color_changed(); - mainframe->preferences_dialog->force_color_changed(); - mainframe->printhost_queue_dlg()->force_color_changed(); -#ifdef _MSW_DARK_MODE - update_scrolls(mainframe); - if (mainframe->is_dlg_layout()) { - // update for tabs bar - UpdateDarkUI(&mainframe->m_settings_dialog); - mainframe->m_settings_dialog.Fit(); - mainframe->m_settings_dialog.Refresh(); - // update scrollbars - update_scrolls(&mainframe->m_settings_dialog); - } -#endif //_MSW_DARK_MODE - } -#endif - mainframe->update_ui_from_settings(); -} - -void GUI_App::persist_window_geometry(wxTopLevelWindow *window, bool default_maximized) -{ - const std::string name = into_u8(window->GetName()); - - window->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent &event) { - window_pos_save(window, name); - event.Skip(); - }); - - window_pos_restore(window, name, default_maximized); - - on_window_geometry(window, [=]() { - window_pos_sanitize(window); - }); -} - -void GUI_App::load_project(wxWindow *parent, wxString& input_file) const -{ - input_file.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one file (3MF/AMF):"), - app_config->get_last_dir(), "", - file_wildcards(FT_PROJECT), wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - input_file = dialog.GetPath(); -} - -void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const -{ - input_files.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one or more files (STL/3MF/STEP/OBJ/AMF/PRUSA):"), - from_u8(app_config->get_last_dir()), "", - file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - dialog.GetPaths(input_files); -} - -void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const -{ - input_file.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one file (GCODE/.GCO/.G/.ngc/NGC):"), - app_config->get_last_dir(), "", - file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - input_file = dialog.GetPath(); -} - -bool GUI_App::switch_language() -{ - if (select_language()) { - recreate_GUI(_L("Changing of an application language") + dots); - return true; - } else { - return false; - } -} - -#ifdef __linux__ -static const wxLanguageInfo* linux_get_existing_locale_language(const wxLanguageInfo* language, - const wxLanguageInfo* system_language) -{ - constexpr size_t max_len = 50; - char path[max_len] = ""; - std::vector locales; - const std::string lang_prefix = into_u8(language->CanonicalName.BeforeFirst('_')); - - // Call locale -a so we can parse the output to get the list of available locales - // We expect lines such as "en_US.utf8". Pick ones starting with the language code - // we are switching to. Lines with different formatting will be removed later. - FILE* fp = popen("locale -a", "r"); - if (fp != NULL) { - while (fgets(path, max_len, fp) != NULL) { - std::string line(path); - line = line.substr(0, line.find('\n')); - if (boost::starts_with(line, lang_prefix)) - locales.push_back(line); - } - pclose(fp); - } - - // locales now contain all candidates for this language. - // Sort them so ones containing anything about UTF-8 are at the end. - std::sort(locales.begin(), locales.end(), [](const std::string& a, const std::string& b) - { - auto has_utf8 = [](const std::string & s) { - auto S = boost::to_upper_copy(s); - return S.find("UTF8") != std::string::npos || S.find("UTF-8") != std::string::npos; - }; - return ! has_utf8(a) && has_utf8(b); - }); - - // Remove the suffix behind a dot, if there is one. - for (std::string& s : locales) - s = s.substr(0, s.find(".")); - - // We just hope that dear Linux "locale -a" returns country codes - // in ISO 3166-1 alpha-2 code (two letter) format. - // https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes - // To be sure, remove anything not looking as expected - // (any number of lowercase letters, underscore, two uppercase letters). - locales.erase(std::remove_if(locales.begin(), - locales.end(), - [](const std::string& s) { - return ! std::regex_match(s, - std::regex("^[a-z]+_[A-Z]{2}$")); - }), - locales.end()); - - if (system_language) { - // Is there a candidate matching a country code of a system language? Move it to the end, - // while maintaining the order of matches, so that the best match ends up at the very end. - std::string system_country = "_" + into_u8(system_language->CanonicalName.AfterFirst('_')).substr(0, 2); - int cnt = locales.size(); - for (int i = 0; i < cnt; ++i) - if (locales[i].find(system_country) != std::string::npos) { - locales.emplace_back(std::move(locales[i])); - locales[i].clear(); - } - } - - // Now try them one by one. - for (auto it = locales.rbegin(); it != locales.rend(); ++ it) - if (! it->empty()) { - const std::string &locale = *it; - const wxLanguageInfo* lang = wxLocale::FindLanguageInfo(from_u8(locale)); - if (wxLocale::IsAvailable(lang->Language)) - return lang; - } - return language; -} -#endif - -int GUI_App::GetSingleChoiceIndex(const wxString& message, - const wxString& caption, - const wxArrayString& choices, - int initialSelection) -{ -#ifdef _WIN32 - wxSingleChoiceDialog dialog(nullptr, message, caption, choices); - wxGetApp().UpdateDlgDarkUI(&dialog); - - dialog.SetSelection(initialSelection); - return dialog.ShowModal() == wxID_OK ? dialog.GetSelection() : -1; -#else - return wxGetSingleChoiceIndex(message, caption, choices, initialSelection); -#endif -} - -// select language from the list of installed languages -bool GUI_App::select_language() -{ - wxArrayString translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY); - std::vector language_infos; - language_infos.emplace_back(wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH)); - for (size_t i = 0; i < translations.GetCount(); ++ i) { - const wxLanguageInfo *langinfo = wxLocale::FindLanguageInfo(translations[i]); - if (langinfo != nullptr) - language_infos.emplace_back(langinfo); - } - sort_remove_duplicates(language_infos); - std::sort(language_infos.begin(), language_infos.end(), [](const wxLanguageInfo* l, const wxLanguageInfo* r) { return l->Description < r->Description; }); - - wxArrayString names; - names.Alloc(language_infos.size()); - - // Some valid language should be selected since the application start up. - const wxLanguage current_language = wxLanguage(m_wxLocale->GetLanguage()); - int init_selection = -1; - int init_selection_alt = -1; - int init_selection_default = -1; - for (size_t i = 0; i < language_infos.size(); ++ i) { - if (wxLanguage(language_infos[i]->Language) == current_language) - // The dictionary matches the active language and country. - init_selection = i; - else if ((language_infos[i]->CanonicalName.BeforeFirst('_') == m_wxLocale->GetCanonicalName().BeforeFirst('_')) || - // if the active language is Slovak, mark the Czech language as active. - (language_infos[i]->CanonicalName.BeforeFirst('_') == "cs" && m_wxLocale->GetCanonicalName().BeforeFirst('_') == "sk")) - // The dictionary matches the active language, it does not necessarily match the country. - init_selection_alt = i; - if (language_infos[i]->CanonicalName.BeforeFirst('_') == "en") - // This will be the default selection if the active language does not match any dictionary. - init_selection_default = i; - names.Add(language_infos[i]->Description); - } - if (init_selection == -1) - // This is the dictionary matching the active language. - init_selection = init_selection_alt; - if (init_selection != -1) - // This is the language to highlight in the choice dialog initially. - init_selection_default = init_selection; - - const long index = GetSingleChoiceIndex(_L("Select the language"), _L("Language"), names, init_selection_default); - // Try to load a new language. - if (index != -1 && (init_selection == -1 || init_selection != index)) { - const wxLanguageInfo *new_language_info = language_infos[index]; - if (this->load_language(new_language_info->CanonicalName, false)) { - // Save language at application config. - // Which language to save as the selected dictionary language? - // 1) Hopefully the language set to wxTranslations by this->load_language(), but that API is weird and we don't want to rely on its - // stability in the future: - // wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); - // 2) Current locale language may not match the dictionary name, see GH issue #3901 - // m_wxLocale->GetCanonicalName() - // 3) new_language_info->CanonicalName is a safe bet. It points to a valid dictionary name. - app_config->set("translation_language", new_language_info->CanonicalName.ToUTF8().data()); - app_config->save(); - return true; - } - } - - return false; -} - -// Load gettext translation files and activate them at the start of the application, -// based on the "translation_language" key stored in the application config. -bool GUI_App::load_language(wxString language, bool initial) -{ - if (initial) { - // There is a static list of lookup path prefixes in wxWidgets. Add ours. - wxFileTranslationsLoader::AddCatalogLookupPathPrefix(from_u8(localization_dir())); - // Get the active language from PrusaSlicer.ini, or empty string if the key does not exist. - language = app_config->get("translation_language"); - if (! language.empty()) - BOOST_LOG_TRIVIAL(trace) << boost::format("translation_language provided by PrusaSlicer.ini: %1%") % language; - - // Get the system language. - { - const wxLanguage lang_system = wxLanguage(wxLocale::GetSystemLanguage()); - if (lang_system != wxLANGUAGE_UNKNOWN) { - m_language_info_system = wxLocale::GetLanguageInfo(lang_system); - BOOST_LOG_TRIVIAL(trace) << boost::format("System language detected (user locales and such): %1%") % m_language_info_system->CanonicalName.ToUTF8().data(); - } - } - { - // Allocating a temporary locale will switch the default wxTranslations to its internal wxTranslations instance. - wxLocale temp_locale; -#ifdef __WXOSX__ - // ysFIXME - temporary workaround till it isn't fixed in wxWidgets: - // Use English as an initial language, because of under OSX it try to load "inappropriate" language for wxLANGUAGE_DEFAULT. - // For example in our case it's trying to load "en_CZ" and as a result PrusaSlicer catch warning message. - // But wxWidgets guys work on it. - temp_locale.Init(wxLANGUAGE_ENGLISH); -#else - temp_locale.Init(); -#endif // __WXOSX__ - // Set the current translation's language to default, otherwise GetBestTranslation() may not work (see the wxWidgets source code). - wxTranslations::Get()->SetLanguage(wxLANGUAGE_DEFAULT); - // Let the wxFileTranslationsLoader enumerate all translation dictionaries for PrusaSlicer - // and try to match them with the system specific "preferred languages". - // There seems to be a support for that on Windows and OSX, while on Linuxes the code just returns wxLocale::GetSystemLanguage(). - // The last parameter gets added to the list of detected dictionaries. This is a workaround - // for not having the English dictionary. Let's hope wxWidgets of various versions process this call the same way. - wxString best_language = wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); - if (! best_language.IsEmpty()) { - m_language_info_best = wxLocale::FindLanguageInfo(best_language); - BOOST_LOG_TRIVIAL(trace) << boost::format("Best translation language detected (may be different from user locales): %1%") % m_language_info_best->CanonicalName.ToUTF8().data(); - } - #ifdef __linux__ - wxString lc_all; - if (wxGetEnv("LC_ALL", &lc_all) && ! lc_all.IsEmpty()) { - // Best language returned by wxWidgets on Linux apparently does not respect LC_ALL. - // Disregard the "best" suggestion in case LC_ALL is provided. - m_language_info_best = nullptr; - } - #endif - } - } - - const wxLanguageInfo *language_info = language.empty() ? nullptr : wxLocale::FindLanguageInfo(language); - if (! language.empty() && (language_info == nullptr || language_info->CanonicalName.empty())) { - // Fix for wxWidgets issue, where the FindLanguageInfo() returns locales with undefined ANSII code (wxLANGUAGE_KONKANI or wxLANGUAGE_MANIPURI). - language_info = nullptr; - BOOST_LOG_TRIVIAL(error) << boost::format("Language code \"%1%\" is not supported") % language.ToUTF8().data(); - } - - if (language_info != nullptr && language_info->LayoutDirection == wxLayout_RightToLeft) { - BOOST_LOG_TRIVIAL(trace) << boost::format("The following language code requires right to left layout, which is not supported by PrusaSlicer: %1%") % language_info->CanonicalName.ToUTF8().data(); - language_info = nullptr; - } - - if (language_info == nullptr) { - // PrusaSlicer does not support the Right to Left languages yet. - if (m_language_info_system != nullptr && m_language_info_system->LayoutDirection != wxLayout_RightToLeft) - language_info = m_language_info_system; - if (m_language_info_best != nullptr && m_language_info_best->LayoutDirection != wxLayout_RightToLeft) - language_info = m_language_info_best; - if (language_info == nullptr) - language_info = wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH_US); - } - - BOOST_LOG_TRIVIAL(trace) << boost::format("Switching wxLocales to %1%") % language_info->CanonicalName.ToUTF8().data(); - - // Alternate language code. - wxLanguage language_dict = wxLanguage(language_info->Language); - if (language_info->CanonicalName.BeforeFirst('_') == "sk") { - // Slovaks understand Czech well. Give them the Czech translation. - language_dict = wxLANGUAGE_CZECH; - BOOST_LOG_TRIVIAL(trace) << "Using Czech dictionaries for Slovak language"; - } - - // Select language for locales. This language may be different from the language of the dictionary. - if (language_info == m_language_info_best || language_info == m_language_info_system) { - // The current language matches user's default profile exactly. That's great. - } else if (m_language_info_best != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_best->CanonicalName.BeforeFirst('_')) { - // Use whatever the operating system recommends, if it the language code of the dictionary matches the recommended language. - // This allows a Swiss guy to use a German dictionary without forcing him to German locales. - language_info = m_language_info_best; - } else if (m_language_info_system != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_system->CanonicalName.BeforeFirst('_')) - language_info = m_language_info_system; - -#ifdef __linux__ - // If we can't find this locale , try to use different one for the language - // instead of just reporting that it is impossible to switch. - if (! wxLocale::IsAvailable(language_info->Language)) { - std::string original_lang = into_u8(language_info->CanonicalName); - language_info = linux_get_existing_locale_language(language_info, m_language_info_system); - BOOST_LOG_TRIVIAL(trace) << boost::format("Can't switch language to %1% (missing locales). Using %2% instead.") - % original_lang % language_info->CanonicalName.ToUTF8().data(); - } -#endif - - if (! wxLocale::IsAvailable(language_info->Language)) { - // Loading the language dictionary failed. - wxString message = "Switching PrusaSlicer to language " + language_info->CanonicalName + " failed."; -#if !defined(_WIN32) && !defined(__APPLE__) - // likely some linux system - message += "\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n"; -#endif - if (initial) - message + "\n\nApplication will close."; - wxMessageBox(message, "PrusaSlicer - Switching language failed", wxOK | wxICON_ERROR); - if (initial) - std::exit(EXIT_FAILURE); - else - return false; - } - - // Release the old locales, create new locales. - //FIXME wxWidgets cause havoc if the current locale is deleted. We just forget it causing memory leaks for now. - m_wxLocale.release(); - m_wxLocale = Slic3r::make_unique(); - m_wxLocale->Init(language_info->Language); - // Override language at the active wxTranslations class (which is stored in the active m_wxLocale) - // to load possibly different dictionary, for example, load Czech dictionary for Slovak language. - wxTranslations::Get()->SetLanguage(language_dict); - m_wxLocale->AddCatalog(SLIC3R_APP_KEY); - m_imgui->set_language(into_u8(language_info->CanonicalName)); - //FIXME This is a temporary workaround, the correct solution is to switch to "C" locale during file import / export only. - //wxSetlocale(LC_NUMERIC, "C"); - Preset::update_suffix_modified((" (" + _L("modified") + ")").ToUTF8().data()); - return true; -} - -Tab* GUI_App::get_tab(Preset::Type type) -{ - for (Tab* tab: tabs_list) - if (tab->type() == type) - return tab->completed() ? tab : nullptr; // To avoid actions with no-completed Tab - return nullptr; -} - -ConfigOptionMode GUI_App::get_mode() -{ - if (!app_config->has("view_mode")) - return comSimple; - - const auto mode = app_config->get("view_mode"); - return mode == "expert" ? comExpert : - mode == "simple" ? comSimple : comAdvanced; -} - -void GUI_App::save_mode(const /*ConfigOptionMode*/int mode) -{ - const std::string mode_str = mode == comExpert ? "expert" : - mode == comSimple ? "simple" : "advanced"; - app_config->set("view_mode", mode_str); - app_config->save(); - update_mode(); -} - -// Update view mode according to selected menu -void GUI_App::update_mode() -{ - sidebar().update_mode(); - -#ifdef _WIN32 //_MSW_DARK_MODE - if (!wxGetApp().tabs_as_menu()) - dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); -#endif - - for (auto tab : tabs_list) - tab->update_mode(); - - plater()->update_menus(); - plater()->canvas3D()->update_gizmos_on_off_state(); -} - -void GUI_App::add_config_menu(wxMenuBar *menu) -{ - auto local_menu = new wxMenu(); - wxWindowID config_id_base = wxWindow::NewControlId(int(ConfigMenuCnt)); - - const auto config_wizard_name = _(ConfigWizard::name(true)); - const auto config_wizard_tooltip = from_u8((boost::format(_utf8(L("Run %s"))) % config_wizard_name).str()); - // Cmd+, is standard on OS X - what about other operating systems? - if (is_editor()) { - local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); - local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots")); - local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot")); - local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); - local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); -#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - //if (DesktopIntegrationDialog::integration_possible()) - local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration")); -#endif //(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - local_menu->AppendSeparator(); - } - local_menu->Append(config_id_base + ConfigMenuPreferences, _L("&Preferences") + dots + -#ifdef __APPLE__ - "\tCtrl+,", -#else - "\tCtrl+P", -#endif - _L("Application preferences")); - wxMenu* mode_menu = nullptr; - if (is_editor()) { - local_menu->AppendSeparator(); - mode_menu = new wxMenu(); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _L("Simple"), _L("Simple View Mode")); -// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _L("Advanced"), _L("Advanced View Mode")); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _CTX(L_CONTEXT("Advanced", "Mode"), "Mode"), _L("Advanced View Mode")); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeExpert, _L("Expert"), _L("Expert View Mode")); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comSimple) evt.Check(true); }, config_id_base + ConfigMenuModeSimple); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comAdvanced) evt.Check(true); }, config_id_base + ConfigMenuModeAdvanced); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comExpert) evt.Check(true); }, config_id_base + ConfigMenuModeExpert); - - local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s View Mode"), SLIC3R_APP_NAME)); - } - local_menu->AppendSeparator(); - local_menu->Append(config_id_base + ConfigMenuLanguage, _L("&Language")); - if (is_editor()) { - local_menu->AppendSeparator(); - local_menu->Append(config_id_base + ConfigMenuFlashFirmware, _L("Flash Printer &Firmware"), _L("Upload a firmware image into an Arduino based printer")); - // TODO: for when we're able to flash dictionaries - // local_menu->Append(config_id_base + FirmwareMenuDict, _L("Flash Language File"), _L("Upload a language dictionary file into a Prusa printer")); - } - - local_menu->Bind(wxEVT_MENU, [this, config_id_base](wxEvent &event) { - switch (event.GetId() - config_id_base) { - case ConfigMenuWizard: - run_wizard(ConfigWizard::RR_USER); - break; - case ConfigMenuUpdateConf: - check_updates(true); - break; - case ConfigMenuUpdateApp: - app_version_check(true); - break; -#ifdef __linux__ - case ConfigMenuDesktopIntegration: - show_desktop_integration_dialog(); - break; -#endif - case ConfigMenuTakeSnapshot: - // Take a configuration snapshot. - if (wxString action_name = _L("Taking a configuration snapshot"); - check_and_save_current_preset_changes(action_name, _L("Some presets are modified and the unsaved changes will not be captured by the configuration snapshot."), false, true)) { - wxTextEntryDialog dlg(nullptr, action_name, _L("Snapshot name")); - UpdateDlgDarkUI(&dlg); - - // set current normal font for dialog children, - // because of just dlg.SetFont(normal_font()) has no result; - for (auto child : dlg.GetChildren()) - child->SetFont(normal_font()); - - if (dlg.ShowModal() == wxID_OK) - if (const Config::Snapshot *snapshot = Config::take_config_snapshot_report_error( - *app_config, Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()); - snapshot != nullptr) - app_config->set("on_snapshot", snapshot->id); - } - break; - case ConfigMenuSnapshots: - if (check_and_save_current_preset_changes(_L("Loading a configuration snapshot"), "", false)) { - std::string on_snapshot; - if (Config::SnapshotDB::singleton().is_on_snapshot(*app_config)) - on_snapshot = app_config->get("on_snapshot"); - ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton(), on_snapshot); - dlg.ShowModal(); - if (!dlg.snapshot_to_activate().empty()) { - if (! Config::SnapshotDB::singleton().is_on_snapshot(*app_config) && - ! Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK, "", - GUI::format(_L("Continue to activate a configuration snapshot %1%?"), dlg.snapshot_to_activate()))) - break; - try { - app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id); - // Enable substitutions, log both user and system substitutions. There should not be any substitutions performed when loading system - // presets because compatibility of profiles shall be verified using the min_slic3r_version keys in config index, but users - // are known to be creative and mess with the config files in various ways. - if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); - ! all_substitutions.empty()) - show_substitutions_info(all_substitutions); - - // Load the currently selected preset into the GUI, update the preset selection box. - load_current_presets(); - } catch (std::exception &ex) { - GUI::show_error(nullptr, _L("Failed to activate configuration snapshot.") + "\n" + into_u8(ex.what())); - } - } - } - break; - case ConfigMenuPreferences: - { - open_preferences(); - break; - } - case ConfigMenuLanguage: - { - /* Before change application language, let's check unsaved changes on 3D-Scene - * and draw user's attention to the application restarting after a language change - */ - { - // the dialog needs to be destroyed before the call to switch_language() - // or sometimes the application crashes into wxDialogBase() destructor - // so we put it into an inner scope - wxString title = is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); - title += " - " + _L("Language selection"); - //wxMessageDialog dialog(nullptr, - MessageDialog dialog(nullptr, - _L("Switching the language will trigger application restart.\n" - "You will lose content of the plater.") + "\n\n" + - _L("Do you want to proceed?"), - title, - wxICON_QUESTION | wxOK | wxCANCEL); - if (dialog.ShowModal() == wxID_CANCEL) - return; - } - - switch_language(); - break; - } - case ConfigMenuFlashFirmware: - FirmwareDialog::run(mainframe); - break; - default: - break; - } - }); - - using std::placeholders::_1; - - if (mode_menu != nullptr) { - auto modfn = [this](int mode, wxCommandEvent&) { if (get_mode() != mode) save_mode(mode); }; - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comSimple, _1), config_id_base + ConfigMenuModeSimple); - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comAdvanced, _1), config_id_base + ConfigMenuModeAdvanced); - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comExpert, _1), config_id_base + ConfigMenuModeExpert); - } - - menu->Append(local_menu, _L("&Configuration")); -} - -void GUI_App::open_preferences(const std::string& highlight_option /*= std::string()*/, const std::string& tab_name/*= std::string()*/) -{ - mainframe->preferences_dialog->show(highlight_option, tab_name); - - if (mainframe->preferences_dialog->recreate_GUI()) - recreate_GUI(_L("Restart application") + dots); - -#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER - if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) -#else - if (mainframe->preferences_dialog->seq_top_layer_only_changed()) -#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER - this->plater_->refresh_print(); - -#ifdef _WIN32 - if (is_editor()) { - if (app_config->get("associate_3mf") == "1") - associate_3mf_files(); - if (app_config->get("associate_stl") == "1") - associate_stl_files(); - } - else { - if (app_config->get("associate_gcode") == "1") - associate_gcode_files(); - } -#endif // _WIN32 - - if (mainframe->preferences_dialog->settings_layout_changed()) { - // hide full main_sizer for mainFrame - mainframe->GetSizer()->Show(false); - mainframe->update_layout(); - mainframe->select_tab(size_t(0)); - } -} - -bool GUI_App::has_unsaved_preset_changes() const -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->saved_preset_is_dirty()) - return true; - } - return false; -} - -bool GUI_App::has_current_preset_changes() const -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) - return true; - } - return false; -} - -void GUI_App::update_saved_preset_from_current_preset() -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (Tab* tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology)) - tab->update_saved_preset_from_current_preset(); - } -} - -std::vector GUI_App::get_active_preset_collections() const -{ - std::vector ret; - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* tab : tabs_list) - if (tab->supports_printer_technology(printer_technology)) - ret.push_back(tab->get_presets()); - return ret; -} - -// To notify the user whether he is aware that some preset changes will be lost, -// UnsavedChangesDialog: "Discard / Save / Cancel" -// This is called when: -// - Close Application & Current project isn't saved -// - Load Project & Current project isn't saved -// - Undo / Redo with change of print technologie -// - Loading snapshot -// - Loading config_file/bundle -// UnsavedChangesDialog: "Don't save / Save / Cancel" -// This is called when: -// - Exporting config_bundle -// - Taking snapshot -bool GUI_App::check_and_save_current_preset_changes(const wxString& caption, const wxString& header, bool remember_choice/* = true*/, bool dont_save_insted_of_discard/* = false*/) -{ - if (has_current_preset_changes()) { - const std::string app_config_key = remember_choice ? "default_action_on_close_application" : ""; - int act_buttons = ActionButtons::SAVE; - if (dont_save_insted_of_discard) - act_buttons |= ActionButtons::DONT_SAVE; - UnsavedChangesDialog dlg(caption, header, app_config_key, act_buttons); - std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); - if (act == "none" && dlg.ShowModal() == wxID_CANCEL) - return false; - - if (dlg.save_preset()) // save selected changes - { - for (const std::pair& nt : dlg.get_names_and_types()) - preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); - - load_current_presets(false); - - // if we saved changes to the new presets, we should to - // synchronize config.ini with the current selections. - preset_bundle->export_selections(*app_config); - - MessageDialog(nullptr, dlg.msg_success_saved_modifications(dlg.get_names_and_types().size())).ShowModal(); - } - } - - return true; -} - -void GUI_App::apply_keeped_preset_modifications() -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (Tab* tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology)) - tab->apply_config_from_cache(); - } - load_current_presets(false); -} - -// This is called when creating new project or load another project -// OR close ConfigWizard -// to ask the user what should we do with unsaved changes for presets. -// New Project => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Cancel" -// => Current project isn't saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" -// Close ConfigWizard => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" -// Note: no_nullptr postponed_apply_of_keeped_changes indicates that thie function is called after ConfigWizard is closed -bool GUI_App::check_and_keep_current_preset_changes(const wxString& caption, const wxString& header, int action_buttons, bool* postponed_apply_of_keeped_changes/* = nullptr*/) -{ - if (has_current_preset_changes()) { - bool is_called_from_configwizard = postponed_apply_of_keeped_changes != nullptr; - - const std::string app_config_key = is_called_from_configwizard ? "" : "default_action_on_new_project"; - UnsavedChangesDialog dlg(caption, header, app_config_key, action_buttons); - std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); - if (act == "none" && dlg.ShowModal() == wxID_CANCEL) - return false; - - auto reset_modifications = [this, is_called_from_configwizard]() { - if (is_called_from_configwizard) - return; // no need to discared changes. It will be done fromConfigWizard closing - - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) - tab->m_presets->discard_current_changes(); - } - load_current_presets(false); - }; - - if (dlg.discard()) - reset_modifications(); - else // save selected changes - { - const auto& preset_names_and_types = dlg.get_names_and_types(); - if (dlg.save_preset()) { - for (const std::pair& nt : preset_names_and_types) - preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); - - // if we saved changes to the new presets, we should to - // synchronize config.ini with the current selections. - preset_bundle->export_selections(*app_config); - - wxString text = dlg.msg_success_saved_modifications(preset_names_and_types.size()); - if (!is_called_from_configwizard) - text += "\n\n" + _L("For new project all modifications will be reseted"); - - MessageDialog(nullptr, text).ShowModal(); - reset_modifications(); - } - else if (dlg.transfer_changes() && (dlg.has_unselected_options() || is_called_from_configwizard)) { - // execute this part of code only if not all modifications are keeping to the new project - // OR this function is called when ConfigWizard is closed and "Keep modifications" is selected - for (const std::pair& nt : preset_names_and_types) { - Preset::Type type = nt.second; - Tab* tab = get_tab(type); - std::vector selected_options = dlg.get_selected_options(type); - if (type == Preset::TYPE_PRINTER) { - auto it = std::find(selected_options.begin(), selected_options.end(), "extruders_count"); - if (it != selected_options.end()) { - // erase "extruders_count" option from the list - selected_options.erase(it); - // cache the extruders count - static_cast(tab)->cache_extruder_cnt(); - } - } - tab->cache_config_diff(selected_options); - if (!is_called_from_configwizard) - tab->m_presets->discard_current_changes(); - } - if (is_called_from_configwizard) - *postponed_apply_of_keeped_changes = true; - else - apply_keeped_preset_modifications(); - } - } - } - - return true; -} - -bool GUI_App::can_load_project() -{ - int saved_project = plater()->save_project_if_dirty(_L("Loading a new project while the current project is modified.")); - if (saved_project == wxID_CANCEL || - (plater()->is_project_dirty() && saved_project == wxID_NO && - !check_and_save_current_preset_changes(_L("Project is loading"), _L("Opening new project while some presets are unsaved.")))) - return false; - return true; -} - -bool GUI_App::check_print_host_queue() -{ - wxString dirty; - std::vector> jobs; - // Get ongoing jobs from dialog - mainframe->m_printhost_queue_dlg->get_active_jobs(jobs); - if (jobs.empty()) - return true; - // Show dialog - wxString job_string = wxString(); - for (const auto& job : jobs) { - job_string += format_wxstr(" %1% : %2% \n", job.first, job.second); - } - wxString message; - message += _(L("The uploads are still ongoing")) + ":\n\n" + job_string +"\n" + _(L("Stop them and continue anyway?")); - //wxMessageDialog dialog(mainframe, - MessageDialog dialog(mainframe, - message, - wxString(SLIC3R_APP_NAME) + " - " + _(L("Ongoing uploads")), - wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); - if (dialog.ShowModal() == wxID_YES) - return true; - - // TODO: If already shown, bring forward - mainframe->m_printhost_queue_dlg->Show(); - return false; -} - -bool GUI_App::checked_tab(Tab* tab) -{ - bool ret = true; - if (find(tabs_list.begin(), tabs_list.end(), tab) == tabs_list.end()) - ret = false; - return ret; -} - -// Update UI / Tabs to reflect changes in the currently loaded presets -void GUI_App::load_current_presets(bool check_printer_presets_ /*= true*/) -{ - // check printer_presets for the containing information about "Print Host upload" - // and create physical printer from it, if any exists - if (check_printer_presets_) - check_printer_presets(); - - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - this->plater()->set_printer_technology(printer_technology); - for (Tab *tab : tabs_list) - if (tab->supports_printer_technology(printer_technology)) { - if (tab->type() == Preset::TYPE_PRINTER) { - static_cast(tab)->update_pages(); - // Mark the plater to update print bed by tab->load_current_preset() from Plater::on_config_change(). - this->plater()->force_print_bed_update(); - } - tab->load_current_preset(); - } -} - -bool GUI_App::OnExceptionInMainLoop() -{ - generic_exception_handle(); - return false; -} - -#ifdef __APPLE__ -// This callback is called from wxEntry()->wxApp::CallOnInit()->NSApplication run -// that is, before GUI_App::OnInit(), so we have a chance to switch GUI_App -// to a G-code viewer. -void GUI_App::OSXStoreOpenFiles(const wxArrayString &fileNames) -{ - size_t num_gcodes = 0; - for (const wxString &filename : fileNames) - if (is_gcode_file(into_u8(filename))) - ++ num_gcodes; - if (fileNames.size() == num_gcodes) { - // Opening PrusaSlicer by drag & dropping a G-Code onto PrusaSlicer icon in Finder, - // just G-codes were passed. Switch to G-code viewer mode. - m_app_mode = EAppMode::GCodeViewer; - unlock_lockfile(get_instance_hash_string() + ".lock", data_dir() + "/cache/"); - if(app_config != nullptr) - delete app_config; - app_config = nullptr; - init_app_config(); - } - wxApp::OSXStoreOpenFiles(fileNames); -} -// wxWidgets override to get an event on open files. -void GUI_App::MacOpenFiles(const wxArrayString &fileNames) -{ - std::vector files; - std::vector gcode_files; - std::vector non_gcode_files; - for (const auto& filename : fileNames) { - if (is_gcode_file(into_u8(filename))) - gcode_files.emplace_back(filename); - else { - files.emplace_back(into_u8(filename)); - non_gcode_files.emplace_back(filename); - } - } - if (m_app_mode == EAppMode::GCodeViewer) { - // Running in G-code viewer. - // Load the first G-code into the G-code viewer. - // Or if no G-codes, send other files to slicer. - if (! gcode_files.empty()) { - if (m_post_initialized) - this->plater()->load_gcode(gcode_files.front()); - else - this->init_params->input_files = { into_u8(gcode_files.front()) }; - } - if (!non_gcode_files.empty()) - start_new_slicer(non_gcode_files, true); - } else { - if (! files.empty()) { - if (m_post_initialized) { - wxArrayString input_files; - for (size_t i = 0; i < non_gcode_files.size(); ++i) - input_files.push_back(non_gcode_files[i]); - this->plater()->load_files(input_files); - } else { - for (const auto &f : non_gcode_files) - this->init_params->input_files.emplace_back(into_u8(f)); - } - } - for (const wxString &filename : gcode_files) - start_new_gcodeviewer(&filename); - } -} -#endif /* __APPLE */ - -Sidebar& GUI_App::sidebar() -{ - return plater_->sidebar(); -} - -ObjectManipulation* GUI_App::obj_manipul() -{ - // If this method is called before plater_ has been initialized, return nullptr (to avoid a crash) - return (plater_ != nullptr) ? sidebar().obj_manipul() : nullptr; -} - -ObjectSettings* GUI_App::obj_settings() -{ - return sidebar().obj_settings(); -} - -ObjectList* GUI_App::obj_list() -{ - return sidebar().obj_list(); -} - -ObjectLayers* GUI_App::obj_layers() -{ - return sidebar().obj_layers(); -} - -Plater* GUI_App::plater() -{ - return plater_; -} - -const Plater* GUI_App::plater() const -{ - return plater_; -} - -Model& GUI_App::model() -{ - return plater_->model(); -} -wxBookCtrlBase* GUI_App::tab_panel() const -{ - return mainframe->m_tabpanel; -} - -NotificationManager * GUI_App::notification_manager() -{ - return plater_->get_notification_manager(); -} - -GalleryDialog* GUI_App::gallery_dialog() -{ - return mainframe->gallery_dialog(); -} - -// extruders count from selected printer preset -int GUI_App::extruders_cnt() const -{ - const Preset& preset = preset_bundle->printers.get_selected_preset(); - return preset.printer_technology() == ptSLA ? 1 : - preset.config.option("nozzle_diameter")->values.size(); -} - -// extruders count from edited printer preset -int GUI_App::extruders_edited_cnt() const -{ - const Preset& preset = preset_bundle->printers.get_edited_preset(); - return preset.printer_technology() == ptSLA ? 1 : - preset.config.option("nozzle_diameter")->values.size(); -} - -wxString GUI_App::current_language_code_safe() const -{ - // Translate the language code to a code, for which Prusa Research maintains translations. - const std::map mapping { - { "cs", "cs_CZ", }, - { "sk", "cs_CZ", }, - { "de", "de_DE", }, - { "es", "es_ES", }, - { "fr", "fr_FR", }, - { "it", "it_IT", }, - { "ja", "ja_JP", }, - { "ko", "ko_KR", }, - { "pl", "pl_PL", }, - { "uk", "uk_UA", }, - { "zh", "zh_CN", }, - { "ru", "ru_RU", }, - }; - wxString language_code = this->current_language_code().BeforeFirst('_'); - auto it = mapping.find(language_code); - if (it != mapping.end()) - language_code = it->second; - else - language_code = "en_US"; - return language_code; -} - -void GUI_App::open_web_page_localized(const std::string &http_address) -{ - open_browser_with_warning_dialog(http_address + "&lng=" + this->current_language_code_safe(), nullptr, false); -} - -// If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have a part(s). -// Because of we can't to print the multi-part objects with SLA technology. -bool GUI_App::may_switch_to_SLA_preset(const wxString& caption) -{ - if (model_has_multi_part_objects(model())) { - show_info(nullptr, - _L("It's impossible to print multi-part object(s) with SLA technology.") + "\n\n" + - _L("Please check your object list before preset changing."), - caption); - return false; - } - return true; -} - -bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page) -{ - wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); - - if (reason == ConfigWizard::RR_USER) { - if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED) - return false; - } - - auto wizard = new ConfigWizard(mainframe); - const bool res = wizard->run(reason, start_page); - - if (res) { - load_current_presets(); - - // #ysFIXME - delete after testing: This part of code looks redundant. All checks are inside ConfigWizard::priv::apply_config() - if (preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) - may_switch_to_SLA_preset(_L("Configuration is editing from ConfigWizard")); - } - - return res; -} - -void GUI_App::show_desktop_integration_dialog() -{ -#ifdef __linux__ - //wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); - DesktopIntegrationDialog dialog(mainframe); - dialog.ShowModal(); -#endif //__linux__ -} - -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG -void GUI_App::gcode_thumbnails_debug() -{ - const std::string BEGIN_MASK = "; thumbnail begin"; - const std::string END_MASK = "; thumbnail end"; - std::string gcode_line; - bool reading_image = false; - unsigned int width = 0; - unsigned int height = 0; - - wxFileDialog dialog(GetTopWindow(), _L("Select a gcode file:"), "", "", "G-code files (*.gcode)|*.gcode;*.GCODE;", wxFD_OPEN | wxFD_FILE_MUST_EXIST); - if (dialog.ShowModal() != wxID_OK) - return; - - std::string in_filename = into_u8(dialog.GetPath()); - std::string out_path = boost::filesystem::path(in_filename).remove_filename().append(L"thumbnail").string(); - - boost::nowide::ifstream in_file(in_filename.c_str()); - std::vector rows; - std::string row; - if (in_file.good()) - { - while (std::getline(in_file, gcode_line)) - { - if (in_file.good()) - { - if (boost::starts_with(gcode_line, BEGIN_MASK)) - { - reading_image = true; - gcode_line = gcode_line.substr(BEGIN_MASK.length() + 1); - std::string::size_type x_pos = gcode_line.find('x'); - std::string width_str = gcode_line.substr(0, x_pos); - width = (unsigned int)::atoi(width_str.c_str()); - std::string height_str = gcode_line.substr(x_pos + 1); - height = (unsigned int)::atoi(height_str.c_str()); - row.clear(); - } - else if (reading_image && boost::starts_with(gcode_line, END_MASK)) - { - std::string out_filename = out_path + std::to_string(width) + "x" + std::to_string(height) + ".png"; - boost::nowide::ofstream out_file(out_filename.c_str(), std::ios::binary); - if (out_file.good()) - { - std::string decoded; - decoded.resize(boost::beast::detail::base64::decoded_size(row.size())); - decoded.resize(boost::beast::detail::base64::decode((void*)&decoded[0], row.data(), row.size()).first); - - out_file.write(decoded.c_str(), decoded.size()); - out_file.close(); - } - - reading_image = false; - width = 0; - height = 0; - rows.clear(); - } - else if (reading_image) - row += gcode_line.substr(2); - } - } - - in_file.close(); - } -} -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG - -void GUI_App::window_pos_save(wxTopLevelWindow* window, const std::string &name) -{ - if (name.empty()) { return; } - const auto config_key = (boost::format("window_%1%") % name).str(); - - WindowMetrics metrics = WindowMetrics::from_window(window); - app_config->set(config_key, metrics.serialize()); - app_config->save(); -} - -void GUI_App::window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized) -{ - if (name.empty()) { return; } - const auto config_key = (boost::format("window_%1%") % name).str(); - - if (! app_config->has(config_key)) { - window->Maximize(default_maximized); - return; - } - - auto metrics = WindowMetrics::deserialize(app_config->get(config_key)); - if (! metrics) { - window->Maximize(default_maximized); - return; - } - - const wxRect& rect = metrics->get_rect(); - - if (app_config->get("restore_win_position") == "1") { - // workaround for crash related to the positioning of the window on secondary monitor - app_config->set("restore_win_position", (boost::format("crashed_at_%1%_pos") % name).str()); - app_config->save(); - window->SetPosition(rect.GetPosition()); - - // workaround for crash related to the positioning of the window on secondary monitor - app_config->set("restore_win_position", (boost::format("crashed_at_%1%_size") % name).str()); - app_config->save(); - window->SetSize(rect.GetSize()); - - // revert "restore_win_position" value if application wasn't crashed - app_config->set("restore_win_position", "1"); - app_config->save(); - } - else - window->CenterOnScreen(); - - window->Maximize(metrics->get_maximized()); -} - -void GUI_App::window_pos_sanitize(wxTopLevelWindow* window) -{ - /*unsigned*/int display_idx = wxDisplay::GetFromWindow(window); - wxRect display; - if (display_idx == wxNOT_FOUND) { - display = wxDisplay(0u).GetClientArea(); - window->Move(display.GetTopLeft()); - } else { - display = wxDisplay(display_idx).GetClientArea(); - } - - auto metrics = WindowMetrics::from_window(window); - metrics.sanitize_for_display(display); - if (window->GetScreenRect() != metrics.get_rect()) { - window->SetSize(metrics.get_rect()); - } -} - -bool GUI_App::config_wizard_startup() -{ - if (!m_app_conf_exists || preset_bundle->printers.only_default_printers()) { - run_wizard(ConfigWizard::RR_DATA_EMPTY); - return true; - } else if (get_app_config()->legacy_datadir()) { - // Looks like user has legacy pre-vendorbundle data directory, - // explain what this is and run the wizard - - MsgDataLegacy dlg; - dlg.ShowModal(); - - run_wizard(ConfigWizard::RR_DATA_LEGACY); - return true; - } - return false; -} - -bool GUI_App::check_updates(const bool verbose) -{ - PresetUpdater::UpdateResult updater_result; - try { - updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION); - if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { - mainframe->Close(); - // Applicaiton is closing. - return false; - } - else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { - m_app_conf_exists = true; - } - else if (verbose && updater_result == PresetUpdater::R_NOOP) { - MsgNoUpdates dlg; - dlg.ShowModal(); - } - } - catch (const std::exception & ex) { - show_error(nullptr, ex.what()); - } - // Applicaiton will continue. - return true; -} - -bool GUI_App::open_browser_with_warning_dialog(const wxString& url, wxWindow* parent/* = nullptr*/, bool force_remember_choice /*= true*/, int flags/* = 0*/) -{ - bool launch = true; - - // warning dialog containes a "Remember my choice" checkbox - std::string option_key = "suppress_hyperlinks"; - if (force_remember_choice || app_config->get(option_key).empty()) { - if (app_config->get(option_key).empty()) { - RichMessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); - dialog.ShowCheckBox(_L("Remember my choice")); - auto answer = dialog.ShowModal(); - launch = answer == wxID_YES; - if (dialog.IsCheckBoxChecked()) { - wxString preferences_item = _L("Suppress to open hyperlink in browser"); - wxString msg = - _L("PrusaSlicer will remember your choice.") + "\n\n" + - _L("You will not be asked about it again on hyperlinks hovering.") + "\n\n" + - format_wxstr(_L("Visit \"Preferences\" and check \"%1%\"\nto changes your choice."), preferences_item); - - MessageDialog msg_dlg(parent, msg, _L("PrusaSlicer: Don't ask me again"), wxOK | wxCANCEL | wxICON_INFORMATION); - if (msg_dlg.ShowModal() == wxID_CANCEL) - return false; - app_config->set(option_key, answer == wxID_NO ? "1" : "0"); - } - } - if (launch) - launch = app_config->get(option_key) != "1"; - } - // warning dialog doesn't containe a "Remember my choice" checkbox - // and will be shown only when "Suppress to open hyperlink in browser" is ON. - else if (app_config->get(option_key) == "1") { - MessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); - launch = dialog.ShowModal() == wxID_YES; - } - - return launch && wxLaunchDefaultBrowser(url, flags); -} - -// static method accepting a wxWindow object as first parameter -// void warning_catcher{ -// my($self, $message_dialog) = @_; -// return sub{ -// my $message = shift; -// return if $message = ~/ GLUquadricObjPtr | Attempt to free unreferenced scalar / ; -// my @params = ($message, 'Warning', wxOK | wxICON_WARNING); -// $message_dialog -// ? $message_dialog->(@params) -// : Wx::MessageDialog->new($self, @params)->ShowModal; -// }; -// } - -// Do we need this function??? -// void GUI_App::notify(message) { -// auto frame = GetTopWindow(); -// // try harder to attract user attention on OS X -// if (!frame->IsActive()) -// frame->RequestUserAttention(defined(__WXOSX__/*&Wx::wxMAC */)? wxUSER_ATTENTION_ERROR : wxUSER_ATTENTION_INFO); -// -// // There used to be notifier using a Growl application for OSX, but Growl is dead. -// // The notifier also supported the Linux X D - bus notifications, but that support was broken. -// //TODO use wxNotificationMessage ? -// } - - -#ifdef __WXMSW__ -void GUI_App::associate_3mf_files() -{ - associate_file_type(L".3mf", L"Prusa.Slicer.1", L"PrusaSlicer", true); -} - -void GUI_App::associate_stl_files() -{ - associate_file_type(L".stl", L"Prusa.Slicer.1", L"PrusaSlicer", true); -} - -void GUI_App::associate_gcode_files() -{ - associate_file_type(L".gcode", L"PrusaSlicer.GCodeViewer.1", L"PrusaSlicerGCodeViewer", true); -} -#endif // __WXMSW__ - -void GUI_App::on_version_read(wxCommandEvent& evt) -{ - app_config->set("version_online", into_u8(evt.GetString())); - app_config->save(); - std::string opt = app_config->get("notify_release"); - if (this->plater_ == nullptr || (opt != "all" && opt != "release")) { - return; - } - if (*Semver::parse(SLIC3R_VERSION) >= *Semver::parse(into_u8(evt.GetString()))) { - return; - } - // notification - /* - this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAvailable - , NotificationManager::NotificationLevel::ImportantNotificationLevel - , Slic3r::format(_u8L("New release version %1% is available."), evt.GetString()) - , _u8L("See Download page.") - , [](wxEvtHandler* evnthndlr) {wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); return true; } - ); - */ - // updater - // read triggered_by_user that was set when calling GUI_App::app_version_check - app_updater(m_app_updater->get_triggered_by_user()); -} - -void GUI_App::app_updater(bool from_user) -{ - DownloadAppData app_data = m_app_updater->get_app_data(); - - if (from_user && (!app_data.version || *app_data.version <= *Semver::parse(SLIC3R_VERSION))) - { - BOOST_LOG_TRIVIAL(info) << "There is no newer version online."; - MsgNoAppUpdates no_update_dialog; - no_update_dialog.ShowModal(); - return; - - } - - assert(!app_data.url.empty()); - assert(!app_data.target_path.empty()); - - // dialog with new version info - AppUpdateAvailableDialog dialog(*Semver::parse(SLIC3R_VERSION), *app_data.version); - auto dialog_result = dialog.ShowModal(); - // checkbox "do not show again" - if (dialog.disable_version_check()) { - app_config->set("notify_release", "none"); - } - // Doesn't wish to update - if (dialog_result != wxID_OK) { - return; - } - // dialog with new version download (installer or app dependent on system) including path selection - AppUpdateDownloadDialog dwnld_dlg(*app_data.version, app_data.target_path); - dialog_result = dwnld_dlg.ShowModal(); - // Doesn't wish to download - if (dialog_result != wxID_OK) { - return; - } - app_data.target_path =dwnld_dlg.get_download_path(); - - // start download - this->plater_->get_notification_manager()->push_download_progress_notification(_utf8("Download"), std::bind(&AppUpdater::cancel_callback, this->m_app_updater.get())); - app_data.start_after = dwnld_dlg.run_after_download(); - m_app_updater->set_app_data(std::move(app_data)); - m_app_updater->sync_download(); -} - -void GUI_App::app_version_check(bool from_user) -{ - if (from_user) { - if (m_app_updater->get_download_ongoing()) { - MessageDialog msgdlg(nullptr, _L("Download of new version is already ongoing. Do you wish to continue?"), _L("Notice"), wxYES_NO); - if (msgdlg.ShowModal() != wxID_YES) - return; - } - } - std::string version_check_url = app_config->version_check_url(); - m_app_updater->sync_version(version_check_url, from_user); -} - -} // GUI -} //Slic3r +#include "libslic3r/Technologies.hpp" +#include "GUI_App.hpp" +#include "GUI_Init.hpp" +#include "GUI_ObjectList.hpp" +#include "GUI_ObjectManipulation.hpp" +#include "GUI_Factories.hpp" +#include "format.hpp" +#include "I18N.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "libslic3r/Utils.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/I18N.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Color.hpp" + +#include "GUI.hpp" +#include "GUI_Utils.hpp" +#include "3DScene.hpp" +#include "MainFrame.hpp" +#include "Plater.hpp" +#include "GLCanvas3D.hpp" + +#include "../Utils/PresetUpdater.hpp" +#include "../Utils/PrintHost.hpp" +#include "../Utils/Process.hpp" +#include "../Utils/MacDarkMode.hpp" +#include "../Utils/AppUpdater.hpp" +#include "../Utils/WinRegistry.hpp" +#include "slic3r/Config/Snapshot.hpp" +#include "ConfigSnapshotDialog.hpp" +#include "FirmwareDialog.hpp" +#include "Preferences.hpp" +#include "Tab.hpp" +#include "SysInfoDialog.hpp" +#include "KBShortcutsDialog.hpp" +#include "UpdateDialogs.hpp" +#include "Mouse3DController.hpp" +#include "RemovableDriveManager.hpp" +#include "InstanceCheck.hpp" +#include "NotificationManager.hpp" +#include "UnsavedChangesDialog.hpp" +#include "SavePresetDialog.hpp" +#include "PrintHostDialogs.hpp" +#include "DesktopIntegrationDialog.hpp" +#include "SendSystemInfoDialog.hpp" + +#include "BitmapCache.hpp" +#include "Notebook.hpp" + +#ifdef __WXMSW__ +#include +#include +#ifdef _MSW_DARK_MODE +#include +#endif // _MSW_DARK_MODE +#endif +#ifdef _WIN32 +#include +#endif + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG +#include +#include +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + +// Needed for forcing menu icons back under gtk2 and gtk3 +#if defined(__WXGTK20__) || defined(__WXGTK3__) + #include +#endif + +using namespace std::literals; + +namespace Slic3r { +namespace GUI { + +class MainFrame; + +class SplashScreen : public wxSplashScreen +{ +public: + SplashScreen(const wxBitmap& bitmap, long splashStyle, int milliseconds, wxPoint pos = wxDefaultPosition) + : wxSplashScreen(bitmap, splashStyle, milliseconds, static_cast(wxGetApp().mainframe), wxID_ANY, wxDefaultPosition, wxDefaultSize, +#ifdef __APPLE__ + wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR | wxSTAY_ON_TOP +#else + wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR +#endif // !__APPLE__ + ) + { + wxASSERT(bitmap.IsOk()); + +// int init_dpi = get_dpi_for_window(this); + this->SetPosition(pos); + // The size of the SplashScreen can be hanged after its moving to another display + // So, update it from a bitmap size + this->SetClientSize(bitmap.GetWidth(), bitmap.GetHeight()); + this->CenterOnScreen(); +// int new_dpi = get_dpi_for_window(this); + +// m_scale = (float)(new_dpi) / (float)(init_dpi); + m_main_bitmap = bitmap; + +// scale_bitmap(m_main_bitmap, m_scale); + + // init constant texts and scale fonts + init_constant_text(); + + // this font will be used for the action string + m_action_font = m_constant_text.credits_font.Bold(); + + // draw logo and constant info text + Decorate(m_main_bitmap); + } + + void SetText(const wxString& text) + { + set_bitmap(m_main_bitmap); + if (!text.empty()) { + wxBitmap bitmap(m_main_bitmap); + + wxMemoryDC memDC; + memDC.SelectObject(bitmap); + + memDC.SetFont(m_action_font); + memDC.SetTextForeground(wxColour(237, 107, 33)); + memDC.DrawText(text, int(m_scale * 60), m_action_line_y_position); + + memDC.SelectObject(wxNullBitmap); + set_bitmap(bitmap); +#ifdef __WXOSX__ + // without this code splash screen wouldn't be updated under OSX + wxYield(); +#endif + } + } + + static wxBitmap MakeBitmap(wxBitmap bmp) + { + if (!bmp.IsOk()) + return wxNullBitmap; + + // create dark grey background for the splashscreen + // It will be 5/3 of the weight of the bitmap + int width = lround((double)5 / 3 * bmp.GetWidth()); + int height = bmp.GetHeight(); + + wxImage image(width, height); + unsigned char* imgdata_ = image.GetData(); + for (int i = 0; i < width * height; ++i) { + *imgdata_++ = 51; + *imgdata_++ = 51; + *imgdata_++ = 51; + } + + wxBitmap new_bmp(image); + + wxMemoryDC memDC; + memDC.SelectObject(new_bmp); + memDC.DrawBitmap(bmp, width - bmp.GetWidth(), 0, true); + + return new_bmp; + } + + void Decorate(wxBitmap& bmp) + { + if (!bmp.IsOk()) + return; + + // draw text to the box at the left of the splashscreen. + // this box will be 2/5 of the weight of the bitmap, and be at the left. + int width = lround(bmp.GetWidth() * 0.4); + + // load bitmap for logo + BitmapCache bmp_cache; + int logo_size = lround(width * 0.25); + wxBitmap logo_bmp = *bmp_cache.load_svg(wxGetApp().logo_name(), logo_size, logo_size); + + wxCoord margin = int(m_scale * 20); + + wxRect banner_rect(wxPoint(0, logo_size), wxPoint(width, bmp.GetHeight())); + banner_rect.Deflate(margin, 2 * margin); + + // use a memory DC to draw directly onto the bitmap + wxMemoryDC memDc(bmp); + + // draw logo + memDc.DrawBitmap(logo_bmp, margin, margin, true); + + // draw the (white) labels inside of our black box (at the left of the splashscreen) + memDc.SetTextForeground(wxColour(255, 255, 255)); + + memDc.SetFont(m_constant_text.title_font); + memDc.DrawLabel(m_constant_text.title, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); + + int title_height = memDc.GetTextExtent(m_constant_text.title).GetY(); + banner_rect.SetTop(banner_rect.GetTop() + title_height); + banner_rect.SetHeight(banner_rect.GetHeight() - title_height); + + memDc.SetFont(m_constant_text.version_font); + memDc.DrawLabel(m_constant_text.version, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); + int version_height = memDc.GetTextExtent(m_constant_text.version).GetY(); + + memDc.SetFont(m_constant_text.credits_font); + memDc.DrawLabel(m_constant_text.credits, banner_rect, wxALIGN_BOTTOM | wxALIGN_LEFT); + int credits_height = memDc.GetMultiLineTextExtent(m_constant_text.credits).GetY(); + int text_height = memDc.GetTextExtent("text").GetY(); + + // calculate position for the dynamic text + int logo_and_header_height = margin + logo_size + title_height + version_height; + m_action_line_y_position = logo_and_header_height + 0.5 * (bmp.GetHeight() - margin - credits_height - logo_and_header_height - text_height); + } + +private: + wxBitmap m_main_bitmap; + wxFont m_action_font; + int m_action_line_y_position; + float m_scale {1.0}; + + struct ConstantText + { + wxString title; + wxString version; + wxString credits; + + wxFont title_font; + wxFont version_font; + wxFont credits_font; + + void init(wxFont init_font) + { + // title + title = wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME; + + // dynamically get the version to display + version = _L("Version") + " " + std::string(SLIC3R_VERSION); + + // credits infornation + credits = title + " " + + _L("is based on Slic3r by Alessandro Ranellucci and the RepRap community.") + "\n" + + _L("Developed by Prusa Research.") + "\n\n" + + title + " " + _L("is licensed under the") + " " + _L("GNU Affero General Public License, version 3") + ".\n\n" + + _L("Contributions by Vojtech Bubnik, Enrico Turri, Oleksandra Iushchenko, Tamas Meszaros, Lukas Matena, Vojtech Kral, David Kocik and numerous others.") + "\n\n" + + _L("Artwork model by Leslie Ing"); + + title_font = version_font = credits_font = init_font; + } + } + m_constant_text; + + void init_constant_text() + { + m_constant_text.init(get_default_font(this)); + + // As default we use a system font for current display. + // Scale fonts in respect to banner width + + int text_banner_width = lround(0.4 * m_main_bitmap.GetWidth()) - roundl(m_scale * 50); // banner_width - margins + + float title_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.title).GetX(); + scale_font(m_constant_text.title_font, title_font_scale > 3.5f ? 3.5f : title_font_scale); + + float version_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.version).GetX(); + scale_font(m_constant_text.version_font, version_font_scale > 2.f ? 2.f : version_font_scale); + + // The width of the credits information string doesn't respect to the banner width some times. + // So, scale credits_font in the respect to the longest string width + int longest_string_width = word_wrap_string(m_constant_text.credits); + float font_scale = (float)text_banner_width / longest_string_width; + scale_font(m_constant_text.credits_font, font_scale); + } + + void set_bitmap(wxBitmap& bmp) + { + m_window->SetBitmap(bmp); + m_window->Refresh(); + m_window->Update(); + } + + void scale_bitmap(wxBitmap& bmp, float scale) + { + if (scale == 1.0) + return; + + wxImage image = bmp.ConvertToImage(); + if (!image.IsOk() || image.GetWidth() == 0 || image.GetHeight() == 0) + return; + + int width = int(scale * image.GetWidth()); + int height = int(scale * image.GetHeight()); + image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); + + bmp = wxBitmap(std::move(image)); + } + + void scale_font(wxFont& font, float scale) + { +#ifdef __WXMSW__ + // Workaround for the font scaling in respect to the current active display, + // not for the primary display, as it's implemented in Font.cpp + // See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/font.cpp + // void wxNativeFontInfo::SetFractionalPointSize(float pointSizeNew) + wxNativeFontInfo nfi= *font.GetNativeFontInfo(); + float pointSizeNew = wxDisplay(this).GetScaleFactor() * scale * font.GetPointSize(); + nfi.lf.lfHeight = nfi.GetLogFontHeightAtPPI(pointSizeNew, get_dpi_for_window(this)); + nfi.pointSize = pointSizeNew; + font = wxFont(nfi); +#else + font.Scale(scale); +#endif //__WXMSW__ + } + + // wrap a string for the strings no longer then 55 symbols + // return extent of the longest string + int word_wrap_string(wxString& input) + { + size_t line_len = 55;// count of symbols in one line + int idx = -1; + size_t cur_len = 0; + + wxString longest_sub_string; + auto get_longest_sub_string = [input](wxString &longest_sub_str, size_t cur_len, size_t i) { + if (cur_len > longest_sub_str.Len()) + longest_sub_str = input.SubString(i - cur_len + 1, i); + }; + + for (size_t i = 0; i < input.Len(); i++) + { + cur_len++; + if (input[i] == ' ') + idx = i; + if (input[i] == '\n') + { + get_longest_sub_string(longest_sub_string, cur_len, i); + idx = -1; + cur_len = 0; + } + if (cur_len >= line_len && idx >= 0) + { + get_longest_sub_string(longest_sub_string, cur_len, i); + input[idx] = '\n'; + cur_len = i - static_cast(idx); + } + } + + return GetTextExtent(longest_sub_string).GetX(); + } +}; + + +#ifdef __linux__ +bool static check_old_linux_datadir(const wxString& app_name) { + // If we are on Linux and the datadir does not exist yet, look into the old + // location where the datadir was before version 2.3. If we find it there, + // tell the user that he might wanna migrate to the new location. + // (https://github.com/prusa3d/PrusaSlicer/issues/2911) + // To be precise, the datadir should exist, it is created when single instance + // lock happens. Instead of checking for existence, check the contents. + + namespace fs = boost::filesystem; + + std::string new_path = Slic3r::data_dir(); + + wxString dir; + if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) + dir = wxFileName::GetHomeDir() + wxS("/.config"); + std::string default_path = (dir + "/" + app_name).ToUTF8().data(); + + if (new_path != default_path) { + // This happens when the user specifies a custom --datadir. + // Do not show anything in that case. + return true; + } + + fs::path data_dir = fs::path(new_path); + if (! fs::is_directory(data_dir)) + return true; // This should not happen. + + int file_count = std::distance(fs::directory_iterator(data_dir), fs::directory_iterator()); + + if (file_count <= 1) { // just cache dir with an instance lock + std::string old_path = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); + + if (fs::is_directory(old_path)) { + wxString msg = from_u8((boost::format(_u8L("Starting with %1% 2.3, configuration " + "directory on Linux has changed (according to XDG Base Directory Specification) to \n%2%.\n\n" + "This directory did not exist yet (maybe you run the new version for the first time).\nHowever, " + "an old %1% configuration directory was detected in \n%3%.\n\n" + "Consider moving the contents of the old directory to the new location in order to access " + "your profiles, etc.\nNote that if you decide to downgrade %1% in future, it will use the old " + "location again.\n\n" + "What do you want to do now?")) % SLIC3R_APP_NAME % new_path % old_path).str()); + wxString caption = from_u8((boost::format(_u8L("%s - BREAKING CHANGE")) % SLIC3R_APP_NAME).str()); + RichMessageDialog dlg(nullptr, msg, caption, wxYES_NO); + dlg.SetYesNoLabels(_L("Quit, I will move my data now"), _L("Start the application")); + if (dlg.ShowModal() != wxID_NO) + return false; + } + } else { + // If the new directory exists, be silent. The user likely already saw the message. + } + return true; +} +#endif + +#ifdef _WIN32 +#if 0 // External Updater is replaced with AppUpdater.cpp +static bool run_updater_win() +{ + // find updater exe + boost::filesystem::path path_updater = boost::dll::program_location().parent_path() / "prusaslicer-updater.exe"; + // run updater. Original args: /silent -restartapp prusa-slicer.exe -startappfirst + std::string msg; + bool res = create_process(path_updater, L"/silent", msg); + if (!res) + BOOST_LOG_TRIVIAL(error) << msg; + return res; +} +#endif // 0 +#endif // _WIN32 + +struct FileWildcards { + std::string_view title; + std::vector file_extensions; +}; + + + +static const FileWildcards file_wildcards_by_type[FT_SIZE] = { + /* FT_STL */ { "STL files"sv, { ".stl"sv } }, + /* FT_OBJ */ { "OBJ files"sv, { ".obj"sv } }, + /* FT_OBJECT */ { "Object files"sv, { ".stl"sv, ".obj"sv } }, + /* FT_STEP */ { "STEP files"sv, { ".stp"sv, ".step"sv } }, + /* FT_AMF */ { "AMF files"sv, { ".amf"sv, ".zip.amf"sv, ".xml"sv } }, + /* FT_3MF */ { "3MF files"sv, { ".3mf"sv } }, + /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv, ".gco"sv, ".g"sv, ".ngc"sv } }, + /* FT_MODEL */ { "Known files"sv, { ".stl"sv, ".obj"sv, ".3mf"sv, ".amf"sv, ".zip.amf"sv, ".xml"sv, ".step"sv, ".stp"sv } }, + /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv, ".amf"sv, ".zip.amf"sv } }, + /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, + + /* FT_INI */ { "INI files"sv, { ".ini"sv } }, + /* FT_SVG */ { "SVG files"sv, { ".svg"sv } }, + + /* FT_TEX */ { "Texture"sv, { ".png"sv, ".svg"sv } }, + + /* FT_SL1 */ { "Masked SLA files"sv, { ".sl1"sv, ".sl1s"sv, ".pwmx"sv } }, +}; + +#if ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR +wxString file_wildcards(FileType file_type) +{ + const FileWildcards& data = file_wildcards_by_type[file_type]; + std::string title; + std::string mask; + + // Generate cumulative first item + for (const std::string_view& ext : data.file_extensions) { + if (title.empty()) { + title = "*"; + title += ext; + mask = title; + } + else { + title += ", *"; + title += ext; + mask += ";*"; + mask += ext; + } + mask += ";*"; + mask += boost::to_upper_copy(std::string(ext)); + } + + wxString ret = GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); + + // Adds an item for each of the extensions + if (data.file_extensions.size() > 1) { + for (const std::string_view& ext : data.file_extensions) { + title = "*"; + title += ext; + ret += GUI::format_wxstr("|%s (%s)|%s", data.title, title, title); + } + } + + return ret; +} +#else +// This function produces a Win32 file dialog file template mask to be consumed by wxWidgets on all platforms. +// The function accepts a custom extension parameter. If the parameter is provided, the custom extension +// will be added as a fist to the list. This is important for a "file save" dialog on OSX, which strips +// an extension from the provided initial file name and substitutes it with the default extension (the first one in the template). +wxString file_wildcards(FileType file_type, const std::string &custom_extension) +{ + const FileWildcards& data = file_wildcards_by_type[file_type]; + std::string title; + std::string mask; + std::string custom_ext_lower; + + if (! custom_extension.empty()) { + // Generate an extension into the title mask and into the list of extensions. + custom_ext_lower = boost::to_lower_copy(custom_extension); + const std::string custom_ext_upper = boost::to_upper_copy(custom_extension); + if (custom_ext_lower == custom_extension) { + // Add a lower case version. + title = std::string("*") + custom_ext_lower; + mask = title; + // Add an upper case version. + mask += ";*"; + mask += custom_ext_upper; + } else if (custom_ext_upper == custom_extension) { + // Add an upper case version. + title = std::string("*") + custom_ext_upper; + mask = title; + // Add a lower case version. + mask += ";*"; + mask += custom_ext_lower; + } else { + // Add the mixed case version only. + title = std::string("*") + custom_extension; + mask = title; + } + } + + for (const std::string_view &ext : data.file_extensions) + // Only add an extension if it was not added first as the custom extension. + if (ext != custom_ext_lower) { + if (title.empty()) { + title = "*"; + title += ext; + mask = title; + } else { + title += ", *"; + title += ext; + mask += ";*"; + mask += ext; + } + mask += ";*"; + mask += boost::to_upper_copy(std::string(ext)); + } + + return GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); +} +#endif // ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR + +static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); } + +#ifdef WIN32 +#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) +static void register_win32_dpi_event() +{ + enum { WM_DPICHANGED_ = 0x02e0 }; + + wxWindow::MSWRegisterMessageHandler(WM_DPICHANGED_, [](wxWindow *win, WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) { + const int dpi = wParam & 0xffff; + const auto rect = reinterpret_cast(lParam); + const wxRect wxrect(wxPoint(rect->top, rect->left), wxPoint(rect->bottom, rect->right)); + + DpiChangedEvent evt(EVT_DPI_CHANGED_SLICER, dpi, wxrect); + win->GetEventHandler()->AddPendingEvent(evt); + + return true; + }); +} +#endif // !wxVERSION_EQUAL_OR_GREATER_THAN + +static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; + +static void register_win32_device_notification_event() +{ + wxWindow::MSWRegisterMessageHandler(WM_DEVICECHANGE, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. + auto main_frame = dynamic_cast(win); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); + if (plater == nullptr) + // Maybe some other top level window like a dialog or maybe a pop-up menu? + return true; + PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; + switch (wParam) { + case DBT_DEVICEARRIVAL: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); + else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { + PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; +// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) { +// printf("DBT_DEVICEARRIVAL %d - Media has arrived: %ws\n", msg_count, lpdbi->dbcc_name); + if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) + plater->GetEventHandler()->AddPendingEvent(HIDDeviceAttachedEvent(EVT_HID_DEVICE_ATTACHED, boost::nowide::narrow(lpdbi->dbcc_name))); + } + break; + case DBT_DEVICEREMOVECOMPLETE: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); + else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { + PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; +// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) +// printf("DBT_DEVICEARRIVAL %d - Media was removed: %ws\n", msg_count, lpdbi->dbcc_name); + if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) + plater->GetEventHandler()->AddPendingEvent(HIDDeviceDetachedEvent(EVT_HID_DEVICE_DETACHED, boost::nowide::narrow(lpdbi->dbcc_name))); + } + break; + default: + break; + } + return true; + }); + + wxWindow::MSWRegisterMessageHandler(MainFrame::WM_USER_MEDIACHANGED, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. + auto main_frame = dynamic_cast(win); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); + if (plater == nullptr) + // Maybe some other top level window like a dialog or maybe a pop-up menu? + return true; + wchar_t sPath[MAX_PATH]; + if (lParam == SHCNE_MEDIAINSERTED || lParam == SHCNE_MEDIAREMOVED) { + struct _ITEMIDLIST* pidl = *reinterpret_cast(wParam); + if (! SHGetPathFromIDList(pidl, sPath)) { + BOOST_LOG_TRIVIAL(error) << "MediaInserted: SHGetPathFromIDList failed"; + return false; + } + } + switch (lParam) { + case SHCNE_MEDIAINSERTED: + { + //printf("SHCNE_MEDIAINSERTED %S\n", sPath); + plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); + break; + } + case SHCNE_MEDIAREMOVED: + { + //printf("SHCNE_MEDIAREMOVED %S\n", sPath); + plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); + break; + } + default: +// printf("Unknown\n"); + break; + } + return true; + }); + + wxWindow::MSWRegisterMessageHandler(WM_INPUT, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + auto main_frame = dynamic_cast(Slic3r::GUI::find_toplevel_parent(win)); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); +// if (wParam == RIM_INPUTSINK && plater != nullptr && main_frame->IsActive()) { + if (wParam == RIM_INPUT && plater != nullptr && main_frame->IsActive()) { + RAWINPUT raw; + UINT rawSize = sizeof(RAWINPUT); + ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &raw, &rawSize, sizeof(RAWINPUTHEADER)); + if (raw.header.dwType == RIM_TYPEHID && plater->get_mouse3d_controller().handle_raw_input_win32(raw.data.hid.bRawData, raw.data.hid.dwSizeHid)) + return true; + } + return false; + }); + + wxWindow::MSWRegisterMessageHandler(WM_COPYDATA, [](wxWindow* win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + COPYDATASTRUCT* copy_data_structure = { 0 }; + copy_data_structure = (COPYDATASTRUCT*)lParam; + if (copy_data_structure->dwData == 1) { + LPCWSTR arguments = (LPCWSTR)copy_data_structure->lpData; + Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(boost::nowide::narrow(arguments)); + } + return true; + }); +} +#endif // WIN32 + +static void generic_exception_handle() +{ + // Note: Some wxWidgets APIs use wxLogError() to report errors, eg. wxImage + // - see https://docs.wxwidgets.org/3.1/classwx_image.html#aa249e657259fe6518d68a5208b9043d0 + // + // wxLogError typically goes around exception handling and display an error dialog some time + // after an error is logged even if exception handling and OnExceptionInMainLoop() take place. + // This is why we use wxLogError() here as well instead of a custom dialog, because it accumulates + // errors if multiple have been collected and displays just one error message for all of them. + // Otherwise we would get multiple error messages for one missing png, for example. + // + // If a custom error message window (or some other solution) were to be used, it would be necessary + // to turn off wxLogError() usage in wx APIs, most notably in wxImage + // - see https://docs.wxwidgets.org/trunk/classwx_image.html#aa32e5d3507cc0f8c3330135bc0befc6a + + try { + throw; + } catch (const std::bad_alloc& ex) { + // bad_alloc in main thread is most likely fatal. Report immediately to the user (wxLogError would be delayed) + // and terminate the app so it is at least certain to happen now. + wxString errmsg = wxString::Format(_L("%s has encountered an error. It was likely caused by running out of memory. " + "If you are sure you have enough RAM on your system, this may also be a bug and we would " + "be glad if you reported it.\n\nThe application will now terminate."), SLIC3R_APP_NAME); + wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Fatal error"), wxOK | wxICON_ERROR); + BOOST_LOG_TRIVIAL(error) << boost::format("std::bad_alloc exception: %1%") % ex.what(); + std::terminate(); + } catch (const boost::io::bad_format_string& ex) { + wxString errmsg = _L("PrusaSlicer has encountered a localization error. " + "Please report to PrusaSlicer team, what language was active and in which scenario " + "this issue happened. Thank you.\n\nThe application will now terminate."); + wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Critical error"), wxOK | wxICON_ERROR); + BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); + std::terminate(); + throw; + } catch (const std::exception& ex) { + wxLogError(format_wxstr(_L("Internal error: %1%"), ex.what())); + BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); + throw; + } +} + +void GUI_App::post_init() +{ + assert(initialized()); + if (! this->initialized()) + throw Slic3r::RuntimeError("Calling post_init() while not yet initialized"); + + if (this->is_gcode_viewer()) { + if (! this->init_params->input_files.empty()) + this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str())); + } + else { + if (! this->init_params->preset_substitutions.empty()) + show_substitutions_info(this->init_params->preset_substitutions); + +#if 0 + // Load the cummulative config over the currently active profiles. + //FIXME if multiple configs are loaded, only the last one will have an effect. + // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). + // As of now only the full configs are supported here. + if (!m_print_config.empty()) + this->gui->mainframe->load_config(m_print_config); +#endif + if (! this->init_params->load_configs.empty()) + // Load the last config to give it a name at the UI. The name of the preset may be later + // changed by loading an AMF or 3MF. + //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. + this->mainframe->load_config_file(this->init_params->load_configs.back()); + // If loading a 3MF file, the config is loaded from the last one. + if (!this->init_params->input_files.empty()) { + const std::vector res = this->plater()->load_files(this->init_params->input_files, true, true); + if (!res.empty() && this->init_params->input_files.size() == 1) { + // Update application titlebar when opening a project file + const std::string& filename = this->init_params->input_files.front(); + if (boost::algorithm::iends_with(filename, ".amf") || + boost::algorithm::iends_with(filename, ".amf.xml") || + boost::algorithm::iends_with(filename, ".3mf")) + this->plater()->set_project_filename(from_u8(filename)); + } + } + if (! this->init_params->extra_config.empty()) + this->mainframe->load_config(this->init_params->extra_config); + } + + // show "Did you know" notification + if (app_config->get("show_hints") == "1" && ! is_gcode_viewer()) + plater_->get_notification_manager()->push_hint_notification(true); + + // The extra CallAfter() is needed because of Mac, where this is the only way + // to popup a modal dialog on start without screwing combo boxes. + // This is ugly but I honestly found no better way to do it. + // Neither wxShowEvent nor wxWindowCreateEvent work reliably. + if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. + if (! this->check_updates(false)) + // Configuration is not compatible and reconfigure was refused by the user. Application is closing. + return; + CallAfter([this] { + bool cw_showed = this->config_wizard_startup(); + this->preset_updater->sync(preset_bundle); + this->app_version_check(false); + if (! cw_showed) { + // The CallAfter is needed as well, without it, GL extensions did not show. + // Also, we only want to show this when the wizard does not, so the new user + // sees something else than "we want something" on the first start. + show_send_system_info_dialog_if_needed(); + } + }); + } + + // Set PrusaSlicer version and save to PrusaSlicer.ini or PrusaSlicerGcodeViewer.ini. + app_config->set("version", SLIC3R_VERSION); + app_config->save(); + +#ifdef _WIN32 + // Sets window property to mainframe so other instances can indentify it. + OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); +#endif //WIN32 +} + +IMPLEMENT_APP(GUI_App) + +GUI_App::GUI_App(EAppMode mode) + : wxApp() + , m_app_mode(mode) + , m_em_unit(10) + , m_imgui(new ImGuiWrapper()) + , m_removable_drive_manager(std::make_unique()) + , m_other_instance_message_handler(std::make_unique()) +{ + //app config initializes early becasuse it is used in instance checking in PrusaSlicer.cpp + this->init_app_config(); + // init app downloader after path to datadir is set + m_app_updater = std::make_unique(); +} + +GUI_App::~GUI_App() +{ + if (app_config != nullptr) + delete app_config; + + if (preset_bundle != nullptr) + delete preset_bundle; + + if (preset_updater != nullptr) + delete preset_updater; +} + +// If formatted for github, plaintext with OpenGL extensions enclosed into
. +// Otherwise HTML formatted for the system info dialog. +std::string GUI_App::get_gl_info(bool for_github) +{ + return OpenGLManager::get_gl_info().to_string(for_github); +} + +wxGLContext* GUI_App::init_glcontext(wxGLCanvas& canvas) +{ +#if ENABLE_GL_CORE_PROFILE +#if ENABLE_OPENGL_DEBUG_OPTION + return m_opengl_mgr.init_glcontext(canvas, init_params != nullptr ? init_params->opengl_version : std::make_pair(0, 0), + init_params != nullptr ? init_params->opengl_debug : false); +#else + return m_opengl_mgr.init_glcontext(canvas, init_params != nullptr ? init_params->opengl_version : std::make_pair(0, 0)); +#endif // ENABLE_OPENGL_DEBUG_OPTION +#else + return m_opengl_mgr.init_glcontext(canvas); +#endif // ENABLE_GL_CORE_PROFILE +} + +bool GUI_App::init_opengl() +{ +#ifdef __linux__ + bool status = m_opengl_mgr.init_gl(); + m_opengl_initialized = true; + return status; +#else + return m_opengl_mgr.init_gl(); +#endif +} + +// gets path to PrusaSlicer.ini, returns semver from first line comment +static boost::optional parse_semver_from_ini(std::string path) +{ + std::ifstream stream(path); + std::stringstream buffer; + buffer << stream.rdbuf(); + std::string body = buffer.str(); + size_t start = body.find("PrusaSlicer "); + if (start == std::string::npos) + return boost::none; + body = body.substr(start + 12); + size_t end = body.find_first_of(" \n"); + if (end < body.size()) + body.resize(end); + return Semver::parse(body); +} + +void GUI_App::init_app_config() +{ + // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. + +// SetAppName(SLIC3R_APP_KEY); + SetAppName(SLIC3R_APP_KEY "-alpha"); +// SetAppName(SLIC3R_APP_KEY "-beta"); + + +// SetAppDisplayName(SLIC3R_APP_NAME); + + // Set the Slic3r data directory at the Slic3r XS module. + // Unix: ~/ .Slic3r + // Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r" + // Mac : "~/Library/Application Support/Slic3r" + + if (data_dir().empty()) { + #ifndef __linux__ + set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()); + #else + // Since version 2.3, config dir on Linux is in ${XDG_CONFIG_HOME}. + // https://github.com/prusa3d/PrusaSlicer/issues/2911 + wxString dir; + if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) + dir = wxFileName::GetHomeDir() + wxS("/.config"); + set_data_dir((dir + "/" + GetAppName()).ToUTF8().data()); + #endif + } else { + m_datadir_redefined = true; + } + + if (!app_config) + app_config = new AppConfig(is_editor() ? AppConfig::EAppMode::Editor : AppConfig::EAppMode::GCodeViewer); + + // load settings + m_app_conf_exists = app_config->exists(); + if (m_app_conf_exists) { + std::string error = app_config->load(); + if (!error.empty()) { + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + if (is_editor()) { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + else { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + } + } +} + +// returns old config path to copy from if such exists, +// returns an empty string if such config path does not exists or if it cannot be loaded. +std::string GUI_App::check_older_app_config(Semver current_version, bool backup) +{ + std::string older_data_dir_path; + + // If the config folder is redefined - do not check + if (m_datadir_redefined) + return {}; + + // find other version app config (alpha / beta / release) + std::string config_path = app_config->config_path(); + boost::filesystem::path parent_file_path(config_path); + std::string filename = parent_file_path.filename().string(); + parent_file_path.remove_filename().remove_filename(); + + std::vector candidates; + + if (SLIC3R_APP_KEY "-alpha" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-alpha" / filename); + if (SLIC3R_APP_KEY "-beta" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-beta" / filename); + if (SLIC3R_APP_KEY != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY / filename); + + Semver last_semver = current_version; + for (const auto& candidate : candidates) { + if (boost::filesystem::exists(candidate)) { + // parse + boost::optionalother_semver = parse_semver_from_ini(candidate.string()); + if (other_semver && *other_semver > last_semver) { + last_semver = *other_semver; + older_data_dir_path = candidate.parent_path().string(); + } + } + } + if (older_data_dir_path.empty()) + return {}; + BOOST_LOG_TRIVIAL(info) << "last app config file used: " << older_data_dir_path; + // ask about using older data folder + + InfoDialog msg(nullptr + , format_wxstr(_L("You are opening %1% version %2%."), SLIC3R_APP_NAME, SLIC3R_VERSION) + , backup ? + format_wxstr(_L( + "The active configuration was created by %1% %2%," + "\nwhile a newer configuration was found in %3%" + "\ncreated by %1% %4%." + "\n\nShall the newer configuration be imported?" + "\nIf so, your active configuration will be backed up before importing the new configuration." + ) + , SLIC3R_APP_NAME, current_version.to_string(), older_data_dir_path, last_semver.to_string()) + : format_wxstr(_L( + "An existing configuration was found in %3%" + "\ncreated by %1% %2%." + "\n\nShall this configuration be imported?" + ) + , SLIC3R_APP_NAME, last_semver.to_string(), older_data_dir_path) + , true, wxYES_NO); + + if (backup) { + msg.SetButtonLabel(wxID_YES, _L("Import")); + msg.SetButtonLabel(wxID_NO, _L("Don't import")); + } + + if (msg.ShowModal() == wxID_YES) { + std::string snapshot_id; + if (backup) { + const Config::Snapshot* snapshot{ nullptr }; + if (! GUI::Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_USER, "", + _u8L("Continue and import newer configuration?"), &snapshot)) + return {}; + if (snapshot) { + // Save snapshot ID before loading the alternate AppConfig, as loading the alternate AppConfig may fail. + snapshot_id = snapshot->id; + assert(! snapshot_id.empty()); + app_config->set("on_snapshot", snapshot_id); + } else + BOOST_LOG_TRIVIAL(error) << "Failed to take congiguration snapshot"; + } + + // load app config from older file + std::string error = app_config->load((boost::filesystem::path(older_data_dir_path) / filename).string()); + if (!error.empty()) { + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + if (is_editor()) { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + else { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + } + if (!snapshot_id.empty()) + app_config->set("on_snapshot", snapshot_id); + m_app_conf_exists = true; + return older_data_dir_path; + } + return {}; +} + +void GUI_App::init_single_instance_checker(const std::string &name, const std::string &path) +{ + BOOST_LOG_TRIVIAL(debug) << "init wx instance checker " << name << " "<< path; + m_single_instance_checker = std::make_unique(boost::nowide::widen(name), boost::nowide::widen(path)); +} + +bool GUI_App::OnInit() +{ + try { + return on_init_inner(); + } catch (const std::exception&) { + generic_exception_handle(); + return false; + } +} + +bool GUI_App::on_init_inner() +{ + // Set initialization of image handlers before any UI actions - See GH issue #7469 + wxInitAllImageHandlers(); + +#if defined(_WIN32) && ! defined(_WIN64) + // Win32 32bit build. + if (wxPlatformInfo::Get().GetArchName().substr(0, 2) == "64") { + RichMessageDialog dlg(nullptr, + _L("You are running a 32 bit build of PrusaSlicer on 64-bit Windows." + "\n32 bit build of PrusaSlicer will likely not be able to utilize all the RAM available in the system." + "\nPlease download and install a 64 bit build of PrusaSlicer from https://www.prusa3d.cz/prusaslicer/." + "\nDo you wish to continue?"), + "PrusaSlicer", wxICON_QUESTION | wxYES_NO); + if (dlg.ShowModal() != wxID_YES) + return false; + } +#endif // _WIN64 + + // Forcing back menu icons under gtk2 and gtk3. Solution is based on: + // https://docs.gtk.org/gtk3/class.Settings.html + // see also https://docs.wxwidgets.org/3.0/classwx_menu_item.html#a2b5d6bcb820b992b1e4709facbf6d4fb + // TODO: Find workaround for GTK4 +#if defined(__WXGTK20__) || defined(__WXGTK3__) + g_object_set (gtk_settings_get_default (), "gtk-menu-images", TRUE, NULL); +#endif + + // Verify resources path + const wxString resources_dir = from_u8(Slic3r::resources_dir()); + wxCHECK_MSG(wxDirExists(resources_dir), false, + wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); + +#ifdef __linux__ + if (! check_old_linux_datadir(GetAppName())) { + std::cerr << "Quitting, user chose to move their data to new location." << std::endl; + return false; + } +#endif + + // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. +// wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0); + // Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible + // performance when working on high resolution multi-display setups. +// wxSystemOptions::SetOption("msw.notebook.themed-background", 0); + +// Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION; + + // !!! Initialization of UI settings as a language, application color mode, fonts... have to be done before first UI action. + // Like here, before the show InfoDialog in check_older_app_config() + + // If load_language() fails, the application closes. + load_language(wxString(), true); +#ifdef _MSW_DARK_MODE + bool init_dark_color_mode = app_config->get("dark_color_mode") == "1"; + bool init_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; + NppDarkMode::InitDarkMode(init_dark_color_mode, init_sys_menu_enabled); +#endif + // initialize label colors and fonts + init_label_colours(); + init_fonts(); + + std::string older_data_dir_path; + if (m_app_conf_exists) { + if (app_config->orig_version().valid() && app_config->orig_version() < *Semver::parse(SLIC3R_VERSION)) + // Only copying configuration if it was saved with a newer slicer than the one currently running. + older_data_dir_path = check_older_app_config(app_config->orig_version(), true); + } else { + // No AppConfig exists, fresh install. Always try to copy from an alternate location, don't make backup of the current configuration. + older_data_dir_path = check_older_app_config(Semver(), false); + } + +#ifdef _MSW_DARK_MODE + // app_config can be updated in check_older_app_config(), so check if dark_color_mode and sys_menu_enabled was changed + if (bool new_dark_color_mode = app_config->get("dark_color_mode") == "1"; + init_dark_color_mode != new_dark_color_mode) { + NppDarkMode::SetDarkMode(new_dark_color_mode); + init_label_colours(); + update_label_colours_from_appconfig(); + } + if (bool new_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; + init_sys_menu_enabled != new_sys_menu_enabled) + NppDarkMode::SetSystemMenuForApp(new_sys_menu_enabled); +#endif + + if (is_editor()) { + std::string msg = Http::tls_global_init(); + std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location"); + bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes" && ssl_cert_store == Http::tls_system_cert_store(); + + if (!msg.empty() && !ssl_accept) { + RichMessageDialog + dlg(nullptr, + wxString::Format(_L("%s\nDo you want to continue?"), msg), + "PrusaSlicer", wxICON_QUESTION | wxYES_NO); + dlg.ShowCheckBox(_L("Remember my choice")); + if (dlg.ShowModal() != wxID_YES) return false; + + app_config->set("tls_cert_store_accepted", + dlg.IsCheckBoxChecked() ? "yes" : "no"); + app_config->set("tls_accepted_cert_store_location", + dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : ""); + } + } + + SplashScreen* scrn = nullptr; + if (app_config->get("show_splash_screen") == "1") { + // make a bitmap with dark grey banner on the left side + wxBitmap bmp = SplashScreen::MakeBitmap(wxBitmap(from_u8(var(is_editor() ? "splashscreen.jpg" : "splashscreen-gcodepreview.jpg")), wxBITMAP_TYPE_JPEG)); + + // Detect position (display) to show the splash screen + // Now this position is equal to the mainframe position + wxPoint splashscreen_pos = wxDefaultPosition; + bool default_splashscreen_pos = true; + if (app_config->has("window_mainframe") && app_config->get("restore_win_position") == "1") { + auto metrics = WindowMetrics::deserialize(app_config->get("window_mainframe")); + default_splashscreen_pos = metrics == boost::none; + if (!default_splashscreen_pos) + splashscreen_pos = metrics->get_rect().GetPosition(); + } + + if (!default_splashscreen_pos) { + // workaround for crash related to the positioning of the window on secondary monitor + get_app_config()->set("restore_win_position", "crashed_at_splashscreen_pos"); + get_app_config()->save(); + } + + // create splash screen with updated bmp + scrn = new SplashScreen(bmp.IsOk() ? bmp : get_bmp_bundle("PrusaSlicer", 400)->GetPreferredBitmapSizeAtScale(1.0), + wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_TIMEOUT, 4000, splashscreen_pos); + + if (!default_splashscreen_pos) + // revert "restore_win_position" value if application wasn't crashed + get_app_config()->set("restore_win_position", "1"); +#ifndef __linux__ + wxYield(); +#endif + scrn->SetText(_L("Loading configuration")+ dots); + } + + preset_bundle = new PresetBundle(); + + // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory + // supplied as argument to --datadir; in that case we should still run the wizard + preset_bundle->setup_directories(); + + if (! older_data_dir_path.empty()) { + preset_bundle->import_newer_configs(older_data_dir_path); + app_config->save(); + } + + if (is_editor()) { +#ifdef __WXMSW__ + if (app_config->get("associate_3mf") == "1") + associate_3mf_files(); + if (app_config->get("associate_stl") == "1") + associate_stl_files(); +#endif // __WXMSW__ + + preset_updater = new PresetUpdater(); + Bind(EVT_SLIC3R_VERSION_ONLINE, &GUI_App::on_version_read, this); + Bind(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, [this](const wxCommandEvent& evt) { + if (this->plater_ != nullptr && app_config->get("notify_release") == "all") { + std::string evt_string = into_u8(evt.GetString()); + if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(evt_string)) { + auto notif_type = (evt_string.find("beta") != std::string::npos ? NotificationType::NewBetaAvailable : NotificationType::NewAlphaAvailable); + this->plater_->get_notification_manager()->push_notification( notif_type + , NotificationManager::NotificationLevel::ImportantNotificationLevel + , Slic3r::format(_u8L("New prerelease version %1% is available."), evt_string) + , _u8L("See Releases page.") + , [](wxEvtHandler* evnthndlr) {wxGetApp().open_browser_with_warning_dialog("https://github.com/prusa3d/PrusaSlicer/releases"); return true; } + ); + } + } + }); + Bind(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, [this](const wxCommandEvent& evt) { + //lm:This does not force a render. The progress bar only updateswhen the mouse is moved. + if (this->plater_ != nullptr) + this->plater_->get_notification_manager()->set_download_progress_percentage((float)std::stoi(into_u8(evt.GetString())) / 100.f ); + }); + + Bind(EVT_SLIC3R_APP_DOWNLOAD_FAILED, [this](const wxCommandEvent& evt) { + if (this->plater_ != nullptr) + this->plater_->get_notification_manager()->close_notification_of_type(NotificationType::AppDownload); + if(!evt.GetString().IsEmpty()) + show_error(nullptr, evt.GetString()); + }); + + Bind(EVT_SLIC3R_APP_OPEN_FAILED, [](const wxCommandEvent& evt) { + show_error(nullptr, evt.GetString()); + }); + } + else { +#ifdef __WXMSW__ + if (app_config->get("associate_gcode") == "1") + associate_gcode_files(); +#endif // __WXMSW__ + } + + // Suppress the '- default -' presets. + preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1"); + try { + // Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only. + // If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force + // installation of a compatible system preset, thus nullifying the system preset substitutions. + init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); + } catch (const std::exception &ex) { + show_error(nullptr, ex.what()); + } + +#ifdef WIN32 +#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) + register_win32_dpi_event(); +#endif // !wxVERSION_EQUAL_OR_GREATER_THAN + register_win32_device_notification_event(); +#endif // WIN32 + + // Let the libslic3r know the callback, which will translate messages on demand. + Slic3r::I18N::set_translate_callback(libslic3r_translate_callback); + + // application frame + if (scrn && is_editor()) + scrn->SetText(_L("Preparing settings tabs") + dots); + + mainframe = new MainFrame(); + // hide settings tabs after first Layout + if (is_editor()) + mainframe->select_tab(size_t(0)); + + sidebar().obj_list()->init_objects(); // propagate model objects to object list +// update_mode(); // !!! do that later + SetTopWindow(mainframe); + + plater_->init_notification_manager(); + + m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); + + if (is_gcode_viewer()) { + mainframe->update_layout(); + if (plater_ != nullptr) + // ensure the selected technology is ptFFF + plater_->set_printer_technology(ptFFF); + } + else + load_current_presets(); + + // Save the active profiles as a "saved into project". + update_saved_preset_from_current_preset(); + + if (plater_ != nullptr) { + // Save the names of active presets and project specific config into ProjectDirtyStateManager. + plater_->reset_project_dirty_initial_presets(); + // Update Project dirty state, update application title bar. + plater_->update_project_dirty_from_presets(); + } + + mainframe->Show(true); + + obj_list()->set_min_height(); + + update_mode(); // update view mode after fix of the object_list size + +#ifdef __APPLE__ + other_instance_message_handler()->bring_instance_forward(); +#endif //__APPLE__ + + Bind(wxEVT_IDLE, [this](wxIdleEvent& event) + { + if (! plater_) + return; + + this->obj_manipul()->update_if_dirty(); + + // An ugly solution to GH #5537 in which GUI_App::init_opengl (normally called from events wxEVT_PAINT + // and wxEVT_SET_FOCUS before GUI_App::post_init is called) wasn't called before GUI_App::post_init and OpenGL wasn't initialized. +#ifdef __linux__ + if (! m_post_initialized && m_opengl_initialized) { +#else + if (! m_post_initialized) { +#endif + m_post_initialized = true; +#ifdef WIN32 + this->mainframe->register_win32_callbacks(); +#endif + this->post_init(); + } + + if (m_post_initialized && app_config->dirty() && app_config->get("autosave") == "1") + app_config->save(); + }); + + m_initialized = true; + + if (const std::string& crash_reason = app_config->get("restore_win_position"); + boost::starts_with(crash_reason,"crashed")) + { + wxString preferences_item = _L("Restore window position on start"); + InfoDialog dialog(nullptr, + _L("PrusaSlicer started after a crash"), + format_wxstr(_L("PrusaSlicer crashed last time when attempting to set window position.\n" + "We are sorry for the inconvenience, it unfortunately happens with certain multiple-monitor setups.\n" + "More precise reason for the crash: \"%1%\".\n" + "For more information see our GitHub issue tracker: \"%2%\" and \"%3%\"\n\n" + "To avoid this problem, consider disabling \"%4%\" in \"Preferences\". " + "Otherwise, the application will most likely crash again next time."), + "" + from_u8(crash_reason) + "", + "#2939", + "#5573", + "" + preferences_item + ""), + true, wxYES_NO); + + dialog.SetButtonLabel(wxID_YES, format_wxstr(_L("Disable \"%1%\""), preferences_item)); + dialog.SetButtonLabel(wxID_NO, format_wxstr(_L("Leave \"%1%\" enabled") , preferences_item)); + + auto answer = dialog.ShowModal(); + if (answer == wxID_YES) + app_config->set("restore_win_position", "0"); + else if (answer == wxID_NO) + app_config->set("restore_win_position", "1"); + app_config->save(); + } + + return true; +} + +unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) +{ + double r = colour.Red(); + double g = colour.Green(); + double b = colour.Blue(); + + return std::round(std::sqrt( + r * r * .241 + + g * g * .691 + + b * b * .068 + )); +} + +bool GUI_App::dark_mode() +{ +#if __APPLE__ + // The check for dark mode returns false positive on 10.12 and 10.13, + // which allowed setting dark menu bar and dock area, which is + // is detected as dark mode. We must run on at least 10.14 where the + // proper dark mode was first introduced. + return wxPlatformInfo::Get().CheckOSVersion(10, 14) && mac_dark_mode(); +#else + if (wxGetApp().app_config->has("dark_color_mode")) + return wxGetApp().app_config->get("dark_color_mode") == "1"; + return check_dark_mode(); +#endif +} + +const wxColour GUI_App::get_label_default_clr_system() +{ + return dark_mode() ? wxColour(115, 220, 103) : wxColour(26, 132, 57); +} + +const wxColour GUI_App::get_label_default_clr_modified() +{ + return dark_mode() ? wxColour(253, 111, 40) : wxColour(252, 77, 1); +} + +void GUI_App::init_label_colours() +{ + m_color_label_modified = get_label_default_clr_modified(); + m_color_label_sys = get_label_default_clr_system(); + + bool is_dark_mode = dark_mode(); +#ifdef _WIN32 + m_color_label_default = is_dark_mode ? wxColour(250, 250, 250): wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + m_color_highlight_label_default = is_dark_mode ? wxColour(230, 230, 230): wxSystemSettings::GetColour(/*wxSYS_COLOUR_HIGHLIGHTTEXT*/wxSYS_COLOUR_WINDOWTEXT); + m_color_highlight_default = is_dark_mode ? wxColour(78, 78, 78) : wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); + m_color_hovered_btn_label = is_dark_mode ? wxColour(253, 111, 40) : wxColour(252, 77, 1); + m_color_default_btn_label = is_dark_mode ? wxColour(255, 181, 100): wxColour(203, 61, 0); + m_color_selected_btn_bg = is_dark_mode ? wxColour(95, 73, 62) : wxColour(228, 220, 216); +#else + m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); +#endif + m_color_window_default = is_dark_mode ? wxColour(43, 43, 43) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); +} + +void GUI_App::update_label_colours_from_appconfig() +{ + if (app_config->has("label_clr_sys")) { + auto str = app_config->get("label_clr_sys"); + if (str != "") + m_color_label_sys = wxColour(str); + } + + if (app_config->has("label_clr_modified")) { + auto str = app_config->get("label_clr_modified"); + if (str != "") + m_color_label_modified = wxColour(str); + } +} + +void GUI_App::update_label_colours() +{ + for (Tab* tab : tabs_list) + tab->update_label_colours(); +} + +#ifdef _WIN32 +static bool is_focused(HWND hWnd) +{ + HWND hFocusedWnd = ::GetFocus(); + return hFocusedWnd && hWnd == hFocusedWnd; +} + +static bool is_default(wxWindow* win) +{ + wxTopLevelWindow* tlw = find_toplevel_parent(win); + if (!tlw) + return false; + + return win == tlw->GetDefaultItem(); +} +#endif + +void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool just_font/* = false*/) +{ +#ifdef _WIN32 + bool is_focused_button = false; + bool is_default_button = false; + if (wxButton* btn = dynamic_cast(window)) { + if (!(btn->GetWindowStyle() & wxNO_BORDER)) { + btn->SetWindowStyle(btn->GetWindowStyle() | wxNO_BORDER); + highlited = true; + } + // button marking + { + auto mark_button = [this, btn, highlited](const bool mark) { + if (btn->GetLabel().IsEmpty()) + btn->SetBackgroundColour(mark ? m_color_selected_btn_bg : highlited ? m_color_highlight_default : m_color_window_default); + else + btn->SetForegroundColour(mark ? m_color_hovered_btn_label : (is_default(btn) ? m_color_default_btn_label : m_color_label_default)); + btn->Refresh(); + btn->Update(); + }; + + // hovering + btn->Bind(wxEVT_ENTER_WINDOW, [mark_button](wxMouseEvent& event) { mark_button(true); event.Skip(); }); + btn->Bind(wxEVT_LEAVE_WINDOW, [mark_button, btn](wxMouseEvent& event) { mark_button(is_focused(btn->GetHWND())); event.Skip(); }); + // focusing + btn->Bind(wxEVT_SET_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(true); event.Skip(); }); + btn->Bind(wxEVT_KILL_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(false); event.Skip(); }); + + is_focused_button = is_focused(btn->GetHWND()); + is_default_button = is_default(btn); + if (is_focused_button || is_default_button) + mark_button(is_focused_button); + } + } + else if (wxTextCtrl* text = dynamic_cast(window)) { + if (text->GetBorder() != wxBORDER_SIMPLE) + text->SetWindowStyle(text->GetWindowStyle() | wxBORDER_SIMPLE); + } + else if (wxCheckListBox* list = dynamic_cast(window)) { + list->SetWindowStyle(list->GetWindowStyle() | wxBORDER_SIMPLE); + list->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + for (size_t i = 0; i < list->GetCount(); i++) + if (wxOwnerDrawn* item = list->GetItem(i)) { + item->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + item->SetTextColour(m_color_label_default); + } + return; + } + else if (dynamic_cast(window)) + window->SetWindowStyle(window->GetWindowStyle() | wxBORDER_SIMPLE); + + if (!just_font) + window->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + if (!is_focused_button && !is_default_button) + window->SetForegroundColour(m_color_label_default); +#endif +} + +// recursive function for scaling fonts for all controls in Window +#ifdef _WIN32 +static void update_dark_children_ui(wxWindow* window, bool just_buttons_update = false) +{ + bool is_btn = dynamic_cast(window) != nullptr; + if (!(just_buttons_update && !is_btn)) + wxGetApp().UpdateDarkUI(window, is_btn); + + auto children = window->GetChildren(); + for (auto child : children) { + update_dark_children_ui(child); + } +} +#endif + +// Note: Don't use this function for Dialog contains ScalableButtons +void GUI_App::UpdateDlgDarkUI(wxDialog* dlg, bool just_buttons_update/* = false*/) +{ +#ifdef _WIN32 + update_dark_children_ui(dlg, just_buttons_update); +#endif +} +void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) +{ +#ifdef _WIN32 + UpdateDarkUI(dvc, highlited ? dark_mode() : false); +#ifdef _MSW_DARK_MODE + dvc->RefreshHeaderDarkMode(&m_normal_font); +#endif //_MSW_DARK_MODE + if (dvc->HasFlag(wxDV_ROW_LINES)) + dvc->SetAlternateRowColour(m_color_highlight_default); + if (dvc->GetBorder() != wxBORDER_SIMPLE) + dvc->SetWindowStyle(dvc->GetWindowStyle() | wxBORDER_SIMPLE); +#endif +} + +void GUI_App::UpdateAllStaticTextDarkUI(wxWindow* parent) +{ +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(parent); + + auto children = parent->GetChildren(); + for (auto child : children) { + if (dynamic_cast(child)) + child->SetForegroundColour(m_color_label_default); + } +#endif +} + +void GUI_App::init_fonts() +{ + m_small_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + m_bold_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold(); + m_normal_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + +#ifdef __WXMAC__ + m_small_font.SetPointSize(11); + m_bold_font.SetPointSize(13); +#endif /*__WXMAC__*/ + + // wxSYS_OEM_FIXED_FONT and wxSYS_ANSI_FIXED_FONT use the same as + // DEFAULT in wxGtk. Use the TELETYPE family as a work-around + m_code_font = wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE)); + m_code_font.SetPointSize(m_normal_font.GetPointSize()); +} + +void GUI_App::update_fonts(const MainFrame *main_frame) +{ + /* Only normal and bold fonts are used for an application rescale, + * because of under MSW small and normal fonts are the same. + * To avoid same rescaling twice, just fill this values + * from rescaled MainFrame + */ + if (main_frame == nullptr) + main_frame = this->mainframe; + m_normal_font = main_frame->normal_font(); + m_small_font = m_normal_font; + m_bold_font = main_frame->normal_font().Bold(); + m_link_font = m_bold_font.Underlined(); + m_em_unit = main_frame->em_unit(); + m_code_font.SetPointSize(m_normal_font.GetPointSize()); +} + +void GUI_App::set_label_clr_modified(const wxColour& clr) +{ + if (m_color_label_modified == clr) + return; + m_color_label_modified = clr; + const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); + app_config->set("label_clr_modified", str); + app_config->save(); +} + +void GUI_App::set_label_clr_sys(const wxColour& clr) +{ + if (m_color_label_sys == clr) + return; + m_color_label_sys = clr; + const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); + app_config->set("label_clr_sys", str); + app_config->save(); +} + +bool GUI_App::tabs_as_menu() const +{ + return app_config->get("tabs_as_menu") == "1"; // || dark_mode(); +} + +wxSize GUI_App::get_min_size() const +{ + return wxSize(76*m_em_unit, 49 * m_em_unit); +} + +float GUI_App::toolbar_icon_scale(const bool is_limited/* = false*/) const +{ +#ifdef __APPLE__ + const float icon_sc = 1.0f; // for Retina display will be used its own scale +#else + const float icon_sc = m_em_unit*0.1f; +#endif // __APPLE__ + + const std::string& use_val = app_config->get("use_custom_toolbar_size"); + const std::string& val = app_config->get("custom_toolbar_size"); + const std::string& auto_val = app_config->get("auto_toolbar_size"); + + if (val.empty() || auto_val.empty() || use_val.empty()) + return icon_sc; + + int int_val = use_val == "0" ? 100 : atoi(val.c_str()); + // correct value in respect to auto_toolbar_size + int_val = std::min(atoi(auto_val.c_str()), int_val); + + if (is_limited && int_val < 50) + int_val = 50; + + return 0.01f * int_val * icon_sc; +} + +void GUI_App::set_auto_toolbar_icon_scale(float scale) const +{ +#ifdef __APPLE__ + const float icon_sc = 1.0f; // for Retina display will be used its own scale +#else + const float icon_sc = m_em_unit * 0.1f; +#endif // __APPLE__ + + long int_val = std::min(int(std::lround(scale / icon_sc * 100)), 100); + std::string val = std::to_string(int_val); + + app_config->set("auto_toolbar_size", val); +} + +// check user printer_presets for the containing information about "Print Host upload" +void GUI_App::check_printer_presets() +{ + std::vector preset_names = PhysicalPrinter::presets_with_print_host_information(preset_bundle->printers); + if (preset_names.empty()) + return; + + wxString msg_text = _L("You have the following presets with saved options for \"Print Host upload\"") + ":"; + for (const std::string& preset_name : preset_names) + msg_text += "\n \"" + from_u8(preset_name) + "\","; + msg_text.RemoveLast(); + msg_text += "\n\n" + _L("But since this version of PrusaSlicer we don't show this information in Printer Settings anymore.\n" + "Settings will be available in physical printers settings.") + "\n\n" + + _L("By default new Printer devices will be named as \"Printer N\" during its creation.\n" + "Note: This name can be changed later from the physical printers settings"); + + //wxMessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); + MessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); + + preset_bundle->physical_printers.load_printers_from_presets(preset_bundle->printers); +} + +void GUI_App::recreate_GUI(const wxString& msg_name) +{ + m_is_recreating_gui = true; + + mainframe->shutdown(); + + wxProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); + dlg.Pulse(); + dlg.Update(10, _L("Recreating") + dots); + + MainFrame *old_main_frame = mainframe; + mainframe = new MainFrame(); + if (is_editor()) + // hide settings tabs after first Layout + mainframe->select_tab(size_t(0)); + // Propagate model objects to object list. + sidebar().obj_list()->init_objects(); + SetTopWindow(mainframe); + + dlg.Update(30, _L("Recreating") + dots); + old_main_frame->Destroy(); + + dlg.Update(80, _L("Loading of current presets") + dots); + m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); + load_current_presets(); + mainframe->Show(true); + + dlg.Update(90, _L("Loading of a mode view") + dots); + + obj_list()->set_min_height(); + update_mode(); + + // #ys_FIXME_delete_after_testing Do we still need this ? +// CallAfter([]() { +// // Run the config wizard, don't offer the "reset user profile" checkbox. +// config_wizard_startup(true); +// }); + + m_is_recreating_gui = false; +} + +void GUI_App::system_info() +{ + SysInfoDialog dlg; + dlg.ShowModal(); +} + +void GUI_App::keyboard_shortcuts() +{ + KBShortcutsDialog dlg; + dlg.ShowModal(); +} + +// static method accepting a wxWindow object as first parameter +bool GUI_App::catch_error(std::function cb, + // wxMessageDialog* message_dialog, + const std::string& err /*= ""*/) +{ + if (!err.empty()) { + if (cb) + cb(); + // if (message_dialog) + // message_dialog->(err, "Error", wxOK | wxICON_ERROR); + show_error(/*this*/nullptr, err); + return true; + } + return false; +} + +// static method accepting a wxWindow object as first parameter +void fatal_error(wxWindow* parent) +{ + show_error(parent, ""); + // exit 1; // #ys_FIXME +} + +#ifdef _WIN32 + +#ifdef _MSW_DARK_MODE +static void update_scrolls(wxWindow* window) +{ + wxWindowList::compatibility_iterator node = window->GetChildren().GetFirst(); + while (node) + { + wxWindow* win = node->GetData(); + if (dynamic_cast(win) || + dynamic_cast(win) || + dynamic_cast(win)) + NppDarkMode::SetDarkExplorerTheme(win->GetHWND()); + + update_scrolls(win); + node = node->GetNext(); + } +} +#endif //_MSW_DARK_MODE + + +#ifdef _MSW_DARK_MODE +void GUI_App::force_menu_update() +{ + NppDarkMode::SetSystemMenuForApp(app_config->get("sys_menu_enabled") == "1"); +} +#endif //_MSW_DARK_MODE + +void GUI_App::force_colors_update() +{ +#ifdef _MSW_DARK_MODE + NppDarkMode::SetDarkMode(app_config->get("dark_color_mode") == "1"); + if (WXHWND wxHWND = wxToolTip::GetToolTipCtrl()) + NppDarkMode::SetDarkExplorerTheme((HWND)wxHWND); + NppDarkMode::SetDarkTitleBar(mainframe->GetHWND()); + NppDarkMode::SetDarkTitleBar(mainframe->m_settings_dialog.GetHWND()); +#endif //_MSW_DARK_MODE + m_force_colors_update = true; +} +#endif //_WIN32 + +// Called after the Preferences dialog is closed and the program settings are saved. +// Update the UI based on the current preferences. +void GUI_App::update_ui_from_settings() +{ + update_label_colours(); +#ifdef _WIN32 + // Upadte UI colors before Update UI from settings + if (m_force_colors_update) { + m_force_colors_update = false; + mainframe->force_color_changed(); + mainframe->diff_dialog.force_color_changed(); + mainframe->preferences_dialog->force_color_changed(); + mainframe->printhost_queue_dlg()->force_color_changed(); +#ifdef _MSW_DARK_MODE + update_scrolls(mainframe); + if (mainframe->is_dlg_layout()) { + // update for tabs bar + UpdateDarkUI(&mainframe->m_settings_dialog); + mainframe->m_settings_dialog.Fit(); + mainframe->m_settings_dialog.Refresh(); + // update scrollbars + update_scrolls(&mainframe->m_settings_dialog); + } +#endif //_MSW_DARK_MODE + } +#endif + mainframe->update_ui_from_settings(); +} + +void GUI_App::persist_window_geometry(wxTopLevelWindow *window, bool default_maximized) +{ + const std::string name = into_u8(window->GetName()); + + window->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent &event) { + window_pos_save(window, name); + event.Skip(); + }); + + window_pos_restore(window, name, default_maximized); + + on_window_geometry(window, [=]() { + window_pos_sanitize(window); + }); +} + +void GUI_App::load_project(wxWindow *parent, wxString& input_file) const +{ + input_file.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one file (3MF/AMF):"), + app_config->get_last_dir(), "", + file_wildcards(FT_PROJECT), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} + +void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const +{ + input_files.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one or more files (STL/3MF/STEP/OBJ/AMF/PRUSA):"), + from_u8(app_config->get_last_dir()), "", + file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + dialog.GetPaths(input_files); +} + +void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const +{ + input_file.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one file (GCODE/.GCO/.G/.ngc/NGC):"), + app_config->get_last_dir(), "", + file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} + +bool GUI_App::switch_language() +{ + if (select_language()) { + recreate_GUI(_L("Changing of an application language") + dots); + return true; + } else { + return false; + } +} + +#ifdef __linux__ +static const wxLanguageInfo* linux_get_existing_locale_language(const wxLanguageInfo* language, + const wxLanguageInfo* system_language) +{ + constexpr size_t max_len = 50; + char path[max_len] = ""; + std::vector locales; + const std::string lang_prefix = into_u8(language->CanonicalName.BeforeFirst('_')); + + // Call locale -a so we can parse the output to get the list of available locales + // We expect lines such as "en_US.utf8". Pick ones starting with the language code + // we are switching to. Lines with different formatting will be removed later. + FILE* fp = popen("locale -a", "r"); + if (fp != NULL) { + while (fgets(path, max_len, fp) != NULL) { + std::string line(path); + line = line.substr(0, line.find('\n')); + if (boost::starts_with(line, lang_prefix)) + locales.push_back(line); + } + pclose(fp); + } + + // locales now contain all candidates for this language. + // Sort them so ones containing anything about UTF-8 are at the end. + std::sort(locales.begin(), locales.end(), [](const std::string& a, const std::string& b) + { + auto has_utf8 = [](const std::string & s) { + auto S = boost::to_upper_copy(s); + return S.find("UTF8") != std::string::npos || S.find("UTF-8") != std::string::npos; + }; + return ! has_utf8(a) && has_utf8(b); + }); + + // Remove the suffix behind a dot, if there is one. + for (std::string& s : locales) + s = s.substr(0, s.find(".")); + + // We just hope that dear Linux "locale -a" returns country codes + // in ISO 3166-1 alpha-2 code (two letter) format. + // https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes + // To be sure, remove anything not looking as expected + // (any number of lowercase letters, underscore, two uppercase letters). + locales.erase(std::remove_if(locales.begin(), + locales.end(), + [](const std::string& s) { + return ! std::regex_match(s, + std::regex("^[a-z]+_[A-Z]{2}$")); + }), + locales.end()); + + if (system_language) { + // Is there a candidate matching a country code of a system language? Move it to the end, + // while maintaining the order of matches, so that the best match ends up at the very end. + std::string system_country = "_" + into_u8(system_language->CanonicalName.AfterFirst('_')).substr(0, 2); + int cnt = locales.size(); + for (int i = 0; i < cnt; ++i) + if (locales[i].find(system_country) != std::string::npos) { + locales.emplace_back(std::move(locales[i])); + locales[i].clear(); + } + } + + // Now try them one by one. + for (auto it = locales.rbegin(); it != locales.rend(); ++ it) + if (! it->empty()) { + const std::string &locale = *it; + const wxLanguageInfo* lang = wxLocale::FindLanguageInfo(from_u8(locale)); + if (wxLocale::IsAvailable(lang->Language)) + return lang; + } + return language; +} +#endif + +int GUI_App::GetSingleChoiceIndex(const wxString& message, + const wxString& caption, + const wxArrayString& choices, + int initialSelection) +{ +#ifdef _WIN32 + wxSingleChoiceDialog dialog(nullptr, message, caption, choices); + wxGetApp().UpdateDlgDarkUI(&dialog); + + dialog.SetSelection(initialSelection); + return dialog.ShowModal() == wxID_OK ? dialog.GetSelection() : -1; +#else + return wxGetSingleChoiceIndex(message, caption, choices, initialSelection); +#endif +} + +// select language from the list of installed languages +bool GUI_App::select_language() +{ + wxArrayString translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY); + std::vector language_infos; + language_infos.emplace_back(wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH)); + for (size_t i = 0; i < translations.GetCount(); ++ i) { + const wxLanguageInfo *langinfo = wxLocale::FindLanguageInfo(translations[i]); + if (langinfo != nullptr) + language_infos.emplace_back(langinfo); + } + sort_remove_duplicates(language_infos); + std::sort(language_infos.begin(), language_infos.end(), [](const wxLanguageInfo* l, const wxLanguageInfo* r) { return l->Description < r->Description; }); + + wxArrayString names; + names.Alloc(language_infos.size()); + + // Some valid language should be selected since the application start up. + const wxLanguage current_language = wxLanguage(m_wxLocale->GetLanguage()); + int init_selection = -1; + int init_selection_alt = -1; + int init_selection_default = -1; + for (size_t i = 0; i < language_infos.size(); ++ i) { + if (wxLanguage(language_infos[i]->Language) == current_language) + // The dictionary matches the active language and country. + init_selection = i; + else if ((language_infos[i]->CanonicalName.BeforeFirst('_') == m_wxLocale->GetCanonicalName().BeforeFirst('_')) || + // if the active language is Slovak, mark the Czech language as active. + (language_infos[i]->CanonicalName.BeforeFirst('_') == "cs" && m_wxLocale->GetCanonicalName().BeforeFirst('_') == "sk")) + // The dictionary matches the active language, it does not necessarily match the country. + init_selection_alt = i; + if (language_infos[i]->CanonicalName.BeforeFirst('_') == "en") + // This will be the default selection if the active language does not match any dictionary. + init_selection_default = i; + names.Add(language_infos[i]->Description); + } + if (init_selection == -1) + // This is the dictionary matching the active language. + init_selection = init_selection_alt; + if (init_selection != -1) + // This is the language to highlight in the choice dialog initially. + init_selection_default = init_selection; + + const long index = GetSingleChoiceIndex(_L("Select the language"), _L("Language"), names, init_selection_default); + // Try to load a new language. + if (index != -1 && (init_selection == -1 || init_selection != index)) { + const wxLanguageInfo *new_language_info = language_infos[index]; + if (this->load_language(new_language_info->CanonicalName, false)) { + // Save language at application config. + // Which language to save as the selected dictionary language? + // 1) Hopefully the language set to wxTranslations by this->load_language(), but that API is weird and we don't want to rely on its + // stability in the future: + // wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); + // 2) Current locale language may not match the dictionary name, see GH issue #3901 + // m_wxLocale->GetCanonicalName() + // 3) new_language_info->CanonicalName is a safe bet. It points to a valid dictionary name. + app_config->set("translation_language", new_language_info->CanonicalName.ToUTF8().data()); + app_config->save(); + return true; + } + } + + return false; +} + +// Load gettext translation files and activate them at the start of the application, +// based on the "translation_language" key stored in the application config. +bool GUI_App::load_language(wxString language, bool initial) +{ + if (initial) { + // There is a static list of lookup path prefixes in wxWidgets. Add ours. + wxFileTranslationsLoader::AddCatalogLookupPathPrefix(from_u8(localization_dir())); + // Get the active language from PrusaSlicer.ini, or empty string if the key does not exist. + language = app_config->get("translation_language"); + if (! language.empty()) + BOOST_LOG_TRIVIAL(trace) << boost::format("translation_language provided by PrusaSlicer.ini: %1%") % language; + + // Get the system language. + { + const wxLanguage lang_system = wxLanguage(wxLocale::GetSystemLanguage()); + if (lang_system != wxLANGUAGE_UNKNOWN) { + m_language_info_system = wxLocale::GetLanguageInfo(lang_system); + BOOST_LOG_TRIVIAL(trace) << boost::format("System language detected (user locales and such): %1%") % m_language_info_system->CanonicalName.ToUTF8().data(); + } + } + { + // Allocating a temporary locale will switch the default wxTranslations to its internal wxTranslations instance. + wxLocale temp_locale; +#ifdef __WXOSX__ + // ysFIXME - temporary workaround till it isn't fixed in wxWidgets: + // Use English as an initial language, because of under OSX it try to load "inappropriate" language for wxLANGUAGE_DEFAULT. + // For example in our case it's trying to load "en_CZ" and as a result PrusaSlicer catch warning message. + // But wxWidgets guys work on it. + temp_locale.Init(wxLANGUAGE_ENGLISH); +#else + temp_locale.Init(); +#endif // __WXOSX__ + // Set the current translation's language to default, otherwise GetBestTranslation() may not work (see the wxWidgets source code). + wxTranslations::Get()->SetLanguage(wxLANGUAGE_DEFAULT); + // Let the wxFileTranslationsLoader enumerate all translation dictionaries for PrusaSlicer + // and try to match them with the system specific "preferred languages". + // There seems to be a support for that on Windows and OSX, while on Linuxes the code just returns wxLocale::GetSystemLanguage(). + // The last parameter gets added to the list of detected dictionaries. This is a workaround + // for not having the English dictionary. Let's hope wxWidgets of various versions process this call the same way. + wxString best_language = wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); + if (! best_language.IsEmpty()) { + m_language_info_best = wxLocale::FindLanguageInfo(best_language); + BOOST_LOG_TRIVIAL(trace) << boost::format("Best translation language detected (may be different from user locales): %1%") % m_language_info_best->CanonicalName.ToUTF8().data(); + } + #ifdef __linux__ + wxString lc_all; + if (wxGetEnv("LC_ALL", &lc_all) && ! lc_all.IsEmpty()) { + // Best language returned by wxWidgets on Linux apparently does not respect LC_ALL. + // Disregard the "best" suggestion in case LC_ALL is provided. + m_language_info_best = nullptr; + } + #endif + } + } + + const wxLanguageInfo *language_info = language.empty() ? nullptr : wxLocale::FindLanguageInfo(language); + if (! language.empty() && (language_info == nullptr || language_info->CanonicalName.empty())) { + // Fix for wxWidgets issue, where the FindLanguageInfo() returns locales with undefined ANSII code (wxLANGUAGE_KONKANI or wxLANGUAGE_MANIPURI). + language_info = nullptr; + BOOST_LOG_TRIVIAL(error) << boost::format("Language code \"%1%\" is not supported") % language.ToUTF8().data(); + } + + if (language_info != nullptr && language_info->LayoutDirection == wxLayout_RightToLeft) { + BOOST_LOG_TRIVIAL(trace) << boost::format("The following language code requires right to left layout, which is not supported by PrusaSlicer: %1%") % language_info->CanonicalName.ToUTF8().data(); + language_info = nullptr; + } + + if (language_info == nullptr) { + // PrusaSlicer does not support the Right to Left languages yet. + if (m_language_info_system != nullptr && m_language_info_system->LayoutDirection != wxLayout_RightToLeft) + language_info = m_language_info_system; + if (m_language_info_best != nullptr && m_language_info_best->LayoutDirection != wxLayout_RightToLeft) + language_info = m_language_info_best; + if (language_info == nullptr) + language_info = wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH_US); + } + + BOOST_LOG_TRIVIAL(trace) << boost::format("Switching wxLocales to %1%") % language_info->CanonicalName.ToUTF8().data(); + + // Alternate language code. + wxLanguage language_dict = wxLanguage(language_info->Language); + if (language_info->CanonicalName.BeforeFirst('_') == "sk") { + // Slovaks understand Czech well. Give them the Czech translation. + language_dict = wxLANGUAGE_CZECH; + BOOST_LOG_TRIVIAL(trace) << "Using Czech dictionaries for Slovak language"; + } + + // Select language for locales. This language may be different from the language of the dictionary. + if (language_info == m_language_info_best || language_info == m_language_info_system) { + // The current language matches user's default profile exactly. That's great. + } else if (m_language_info_best != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_best->CanonicalName.BeforeFirst('_')) { + // Use whatever the operating system recommends, if it the language code of the dictionary matches the recommended language. + // This allows a Swiss guy to use a German dictionary without forcing him to German locales. + language_info = m_language_info_best; + } else if (m_language_info_system != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_system->CanonicalName.BeforeFirst('_')) + language_info = m_language_info_system; + +#ifdef __linux__ + // If we can't find this locale , try to use different one for the language + // instead of just reporting that it is impossible to switch. + if (! wxLocale::IsAvailable(language_info->Language)) { + std::string original_lang = into_u8(language_info->CanonicalName); + language_info = linux_get_existing_locale_language(language_info, m_language_info_system); + BOOST_LOG_TRIVIAL(trace) << boost::format("Can't switch language to %1% (missing locales). Using %2% instead.") + % original_lang % language_info->CanonicalName.ToUTF8().data(); + } +#endif + + if (! wxLocale::IsAvailable(language_info->Language)) { + // Loading the language dictionary failed. + wxString message = "Switching PrusaSlicer to language " + language_info->CanonicalName + " failed."; +#if !defined(_WIN32) && !defined(__APPLE__) + // likely some linux system + message += "\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n"; +#endif + if (initial) + message + "\n\nApplication will close."; + wxMessageBox(message, "PrusaSlicer - Switching language failed", wxOK | wxICON_ERROR); + if (initial) + std::exit(EXIT_FAILURE); + else + return false; + } + + // Release the old locales, create new locales. + //FIXME wxWidgets cause havoc if the current locale is deleted. We just forget it causing memory leaks for now. + m_wxLocale.release(); + m_wxLocale = Slic3r::make_unique(); + m_wxLocale->Init(language_info->Language); + // Override language at the active wxTranslations class (which is stored in the active m_wxLocale) + // to load possibly different dictionary, for example, load Czech dictionary for Slovak language. + wxTranslations::Get()->SetLanguage(language_dict); + m_wxLocale->AddCatalog(SLIC3R_APP_KEY); + m_imgui->set_language(into_u8(language_info->CanonicalName)); + //FIXME This is a temporary workaround, the correct solution is to switch to "C" locale during file import / export only. + //wxSetlocale(LC_NUMERIC, "C"); + Preset::update_suffix_modified((" (" + _L("modified") + ")").ToUTF8().data()); + return true; +} + +Tab* GUI_App::get_tab(Preset::Type type) +{ + for (Tab* tab: tabs_list) + if (tab->type() == type) + return tab->completed() ? tab : nullptr; // To avoid actions with no-completed Tab + return nullptr; +} + +ConfigOptionMode GUI_App::get_mode() +{ + if (!app_config->has("view_mode")) + return comSimple; + + const auto mode = app_config->get("view_mode"); + return mode == "expert" ? comExpert : + mode == "simple" ? comSimple : comAdvanced; +} + +void GUI_App::save_mode(const /*ConfigOptionMode*/int mode) +{ + const std::string mode_str = mode == comExpert ? "expert" : + mode == comSimple ? "simple" : "advanced"; + app_config->set("view_mode", mode_str); + app_config->save(); + update_mode(); +} + +// Update view mode according to selected menu +void GUI_App::update_mode() +{ + sidebar().update_mode(); + +#ifdef _WIN32 //_MSW_DARK_MODE + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); +#endif + + for (auto tab : tabs_list) + tab->update_mode(); + + plater()->update_menus(); + plater()->canvas3D()->update_gizmos_on_off_state(); +} + +void GUI_App::add_config_menu(wxMenuBar *menu) +{ + auto local_menu = new wxMenu(); + wxWindowID config_id_base = wxWindow::NewControlId(int(ConfigMenuCnt)); + + const auto config_wizard_name = _(ConfigWizard::name(true)); + const auto config_wizard_tooltip = from_u8((boost::format(_utf8(L("Run %s"))) % config_wizard_name).str()); + // Cmd+, is standard on OS X - what about other operating systems? + if (is_editor()) { + local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); + local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots")); + local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot")); + local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); + local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); +#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + //if (DesktopIntegrationDialog::integration_possible()) + local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration")); +#endif //(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + local_menu->AppendSeparator(); + } + local_menu->Append(config_id_base + ConfigMenuPreferences, _L("&Preferences") + dots + +#ifdef __APPLE__ + "\tCtrl+,", +#else + "\tCtrl+P", +#endif + _L("Application preferences")); + wxMenu* mode_menu = nullptr; + if (is_editor()) { + local_menu->AppendSeparator(); + mode_menu = new wxMenu(); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _L("Simple"), _L("Simple View Mode")); +// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _L("Advanced"), _L("Advanced View Mode")); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _CTX(L_CONTEXT("Advanced", "Mode"), "Mode"), _L("Advanced View Mode")); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeExpert, _L("Expert"), _L("Expert View Mode")); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comSimple) evt.Check(true); }, config_id_base + ConfigMenuModeSimple); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comAdvanced) evt.Check(true); }, config_id_base + ConfigMenuModeAdvanced); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comExpert) evt.Check(true); }, config_id_base + ConfigMenuModeExpert); + + local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s View Mode"), SLIC3R_APP_NAME)); + } + local_menu->AppendSeparator(); + local_menu->Append(config_id_base + ConfigMenuLanguage, _L("&Language")); + if (is_editor()) { + local_menu->AppendSeparator(); + local_menu->Append(config_id_base + ConfigMenuFlashFirmware, _L("Flash Printer &Firmware"), _L("Upload a firmware image into an Arduino based printer")); + // TODO: for when we're able to flash dictionaries + // local_menu->Append(config_id_base + FirmwareMenuDict, _L("Flash Language File"), _L("Upload a language dictionary file into a Prusa printer")); + } + + local_menu->Bind(wxEVT_MENU, [this, config_id_base](wxEvent &event) { + switch (event.GetId() - config_id_base) { + case ConfigMenuWizard: + run_wizard(ConfigWizard::RR_USER); + break; + case ConfigMenuUpdateConf: + check_updates(true); + break; + case ConfigMenuUpdateApp: + app_version_check(true); + break; +#ifdef __linux__ + case ConfigMenuDesktopIntegration: + show_desktop_integration_dialog(); + break; +#endif + case ConfigMenuTakeSnapshot: + // Take a configuration snapshot. + if (wxString action_name = _L("Taking a configuration snapshot"); + check_and_save_current_preset_changes(action_name, _L("Some presets are modified and the unsaved changes will not be captured by the configuration snapshot."), false, true)) { + wxTextEntryDialog dlg(nullptr, action_name, _L("Snapshot name")); + UpdateDlgDarkUI(&dlg); + + // set current normal font for dialog children, + // because of just dlg.SetFont(normal_font()) has no result; + for (auto child : dlg.GetChildren()) + child->SetFont(normal_font()); + + if (dlg.ShowModal() == wxID_OK) + if (const Config::Snapshot *snapshot = Config::take_config_snapshot_report_error( + *app_config, Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()); + snapshot != nullptr) + app_config->set("on_snapshot", snapshot->id); + } + break; + case ConfigMenuSnapshots: + if (check_and_save_current_preset_changes(_L("Loading a configuration snapshot"), "", false)) { + std::string on_snapshot; + if (Config::SnapshotDB::singleton().is_on_snapshot(*app_config)) + on_snapshot = app_config->get("on_snapshot"); + ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton(), on_snapshot); + dlg.ShowModal(); + if (!dlg.snapshot_to_activate().empty()) { + if (! Config::SnapshotDB::singleton().is_on_snapshot(*app_config) && + ! Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK, "", + GUI::format(_L("Continue to activate a configuration snapshot %1%?"), dlg.snapshot_to_activate()))) + break; + try { + app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id); + // Enable substitutions, log both user and system substitutions. There should not be any substitutions performed when loading system + // presets because compatibility of profiles shall be verified using the min_slic3r_version keys in config index, but users + // are known to be creative and mess with the config files in various ways. + if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); + ! all_substitutions.empty()) + show_substitutions_info(all_substitutions); + + // Load the currently selected preset into the GUI, update the preset selection box. + load_current_presets(); + } catch (std::exception &ex) { + GUI::show_error(nullptr, _L("Failed to activate configuration snapshot.") + "\n" + into_u8(ex.what())); + } + } + } + break; + case ConfigMenuPreferences: + { + open_preferences(); + break; + } + case ConfigMenuLanguage: + { + /* Before change application language, let's check unsaved changes on 3D-Scene + * and draw user's attention to the application restarting after a language change + */ + { + // the dialog needs to be destroyed before the call to switch_language() + // or sometimes the application crashes into wxDialogBase() destructor + // so we put it into an inner scope + wxString title = is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); + title += " - " + _L("Language selection"); + //wxMessageDialog dialog(nullptr, + MessageDialog dialog(nullptr, + _L("Switching the language will trigger application restart.\n" + "You will lose content of the plater.") + "\n\n" + + _L("Do you want to proceed?"), + title, + wxICON_QUESTION | wxOK | wxCANCEL); + if (dialog.ShowModal() == wxID_CANCEL) + return; + } + + switch_language(); + break; + } + case ConfigMenuFlashFirmware: + FirmwareDialog::run(mainframe); + break; + default: + break; + } + }); + + using std::placeholders::_1; + + if (mode_menu != nullptr) { + auto modfn = [this](int mode, wxCommandEvent&) { if (get_mode() != mode) save_mode(mode); }; + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comSimple, _1), config_id_base + ConfigMenuModeSimple); + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comAdvanced, _1), config_id_base + ConfigMenuModeAdvanced); + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comExpert, _1), config_id_base + ConfigMenuModeExpert); + } + + menu->Append(local_menu, _L("&Configuration")); +} + +void GUI_App::open_preferences(const std::string& highlight_option /*= std::string()*/, const std::string& tab_name/*= std::string()*/) +{ + mainframe->preferences_dialog->show(highlight_option, tab_name); + + if (mainframe->preferences_dialog->recreate_GUI()) + recreate_GUI(_L("Restart application") + dots); + +#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER + if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) +#else + if (mainframe->preferences_dialog->seq_top_layer_only_changed()) +#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER + this->plater_->refresh_print(); + +#ifdef _WIN32 + if (is_editor()) { + if (app_config->get("associate_3mf") == "1") + associate_3mf_files(); + if (app_config->get("associate_stl") == "1") + associate_stl_files(); + } + else { + if (app_config->get("associate_gcode") == "1") + associate_gcode_files(); + } +#endif // _WIN32 + + if (mainframe->preferences_dialog->settings_layout_changed()) { + // hide full main_sizer for mainFrame + mainframe->GetSizer()->Show(false); + mainframe->update_layout(); + mainframe->select_tab(size_t(0)); + } +} + +bool GUI_App::has_unsaved_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->saved_preset_is_dirty()) + return true; + } + return false; +} + +bool GUI_App::has_current_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + return true; + } + return false; +} + +void GUI_App::update_saved_preset_from_current_preset() +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) + tab->update_saved_preset_from_current_preset(); + } +} + +std::vector GUI_App::get_active_preset_collections() const +{ + std::vector ret; + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* tab : tabs_list) + if (tab->supports_printer_technology(printer_technology)) + ret.push_back(tab->get_presets()); + return ret; +} + +// To notify the user whether he is aware that some preset changes will be lost, +// UnsavedChangesDialog: "Discard / Save / Cancel" +// This is called when: +// - Close Application & Current project isn't saved +// - Load Project & Current project isn't saved +// - Undo / Redo with change of print technologie +// - Loading snapshot +// - Loading config_file/bundle +// UnsavedChangesDialog: "Don't save / Save / Cancel" +// This is called when: +// - Exporting config_bundle +// - Taking snapshot +bool GUI_App::check_and_save_current_preset_changes(const wxString& caption, const wxString& header, bool remember_choice/* = true*/, bool dont_save_insted_of_discard/* = false*/) +{ + if (has_current_preset_changes()) { + const std::string app_config_key = remember_choice ? "default_action_on_close_application" : ""; + int act_buttons = ActionButtons::SAVE; + if (dont_save_insted_of_discard) + act_buttons |= ActionButtons::DONT_SAVE; + UnsavedChangesDialog dlg(caption, header, app_config_key, act_buttons); + std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); + if (act == "none" && dlg.ShowModal() == wxID_CANCEL) + return false; + + if (dlg.save_preset()) // save selected changes + { + for (const std::pair& nt : dlg.get_names_and_types()) + preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); + + load_current_presets(false); + + // if we saved changes to the new presets, we should to + // synchronize config.ini with the current selections. + preset_bundle->export_selections(*app_config); + + MessageDialog(nullptr, dlg.msg_success_saved_modifications(dlg.get_names_and_types().size())).ShowModal(); + } + } + + return true; +} + +void GUI_App::apply_keeped_preset_modifications() +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) + tab->apply_config_from_cache(); + } + load_current_presets(false); +} + +// This is called when creating new project or load another project +// OR close ConfigWizard +// to ask the user what should we do with unsaved changes for presets. +// New Project => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Cancel" +// => Current project isn't saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" +// Close ConfigWizard => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" +// Note: no_nullptr postponed_apply_of_keeped_changes indicates that thie function is called after ConfigWizard is closed +bool GUI_App::check_and_keep_current_preset_changes(const wxString& caption, const wxString& header, int action_buttons, bool* postponed_apply_of_keeped_changes/* = nullptr*/) +{ + if (has_current_preset_changes()) { + bool is_called_from_configwizard = postponed_apply_of_keeped_changes != nullptr; + + const std::string app_config_key = is_called_from_configwizard ? "" : "default_action_on_new_project"; + UnsavedChangesDialog dlg(caption, header, app_config_key, action_buttons); + std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); + if (act == "none" && dlg.ShowModal() == wxID_CANCEL) + return false; + + auto reset_modifications = [this, is_called_from_configwizard]() { + if (is_called_from_configwizard) + return; // no need to discared changes. It will be done fromConfigWizard closing + + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + tab->m_presets->discard_current_changes(); + } + load_current_presets(false); + }; + + if (dlg.discard()) + reset_modifications(); + else // save selected changes + { + const auto& preset_names_and_types = dlg.get_names_and_types(); + if (dlg.save_preset()) { + for (const std::pair& nt : preset_names_and_types) + preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); + + // if we saved changes to the new presets, we should to + // synchronize config.ini with the current selections. + preset_bundle->export_selections(*app_config); + + wxString text = dlg.msg_success_saved_modifications(preset_names_and_types.size()); + if (!is_called_from_configwizard) + text += "\n\n" + _L("For new project all modifications will be reseted"); + + MessageDialog(nullptr, text).ShowModal(); + reset_modifications(); + } + else if (dlg.transfer_changes() && (dlg.has_unselected_options() || is_called_from_configwizard)) { + // execute this part of code only if not all modifications are keeping to the new project + // OR this function is called when ConfigWizard is closed and "Keep modifications" is selected + for (const std::pair& nt : preset_names_and_types) { + Preset::Type type = nt.second; + Tab* tab = get_tab(type); + std::vector selected_options = dlg.get_selected_options(type); + if (type == Preset::TYPE_PRINTER) { + auto it = std::find(selected_options.begin(), selected_options.end(), "extruders_count"); + if (it != selected_options.end()) { + // erase "extruders_count" option from the list + selected_options.erase(it); + // cache the extruders count + static_cast(tab)->cache_extruder_cnt(); + } + } + tab->cache_config_diff(selected_options); + if (!is_called_from_configwizard) + tab->m_presets->discard_current_changes(); + } + if (is_called_from_configwizard) + *postponed_apply_of_keeped_changes = true; + else + apply_keeped_preset_modifications(); + } + } + } + + return true; +} + +bool GUI_App::can_load_project() +{ + int saved_project = plater()->save_project_if_dirty(_L("Loading a new project while the current project is modified.")); + if (saved_project == wxID_CANCEL || + (plater()->is_project_dirty() && saved_project == wxID_NO && + !check_and_save_current_preset_changes(_L("Project is loading"), _L("Opening new project while some presets are unsaved.")))) + return false; + return true; +} + +bool GUI_App::check_print_host_queue() +{ + wxString dirty; + std::vector> jobs; + // Get ongoing jobs from dialog + mainframe->m_printhost_queue_dlg->get_active_jobs(jobs); + if (jobs.empty()) + return true; + // Show dialog + wxString job_string = wxString(); + for (const auto& job : jobs) { + job_string += format_wxstr(" %1% : %2% \n", job.first, job.second); + } + wxString message; + message += _(L("The uploads are still ongoing")) + ":\n\n" + job_string +"\n" + _(L("Stop them and continue anyway?")); + //wxMessageDialog dialog(mainframe, + MessageDialog dialog(mainframe, + message, + wxString(SLIC3R_APP_NAME) + " - " + _(L("Ongoing uploads")), + wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); + if (dialog.ShowModal() == wxID_YES) + return true; + + // TODO: If already shown, bring forward + mainframe->m_printhost_queue_dlg->Show(); + return false; +} + +bool GUI_App::checked_tab(Tab* tab) +{ + bool ret = true; + if (find(tabs_list.begin(), tabs_list.end(), tab) == tabs_list.end()) + ret = false; + return ret; +} + +// Update UI / Tabs to reflect changes in the currently loaded presets +void GUI_App::load_current_presets(bool check_printer_presets_ /*= true*/) +{ + // check printer_presets for the containing information about "Print Host upload" + // and create physical printer from it, if any exists + if (check_printer_presets_) + check_printer_presets(); + + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + this->plater()->set_printer_technology(printer_technology); + for (Tab *tab : tabs_list) + if (tab->supports_printer_technology(printer_technology)) { + if (tab->type() == Preset::TYPE_PRINTER) { + static_cast(tab)->update_pages(); + // Mark the plater to update print bed by tab->load_current_preset() from Plater::on_config_change(). + this->plater()->force_print_bed_update(); + } + tab->load_current_preset(); + } +} + +bool GUI_App::OnExceptionInMainLoop() +{ + generic_exception_handle(); + return false; +} + +#ifdef __APPLE__ +// This callback is called from wxEntry()->wxApp::CallOnInit()->NSApplication run +// that is, before GUI_App::OnInit(), so we have a chance to switch GUI_App +// to a G-code viewer. +void GUI_App::OSXStoreOpenFiles(const wxArrayString &fileNames) +{ + size_t num_gcodes = 0; + for (const wxString &filename : fileNames) + if (is_gcode_file(into_u8(filename))) + ++ num_gcodes; + if (fileNames.size() == num_gcodes) { + // Opening PrusaSlicer by drag & dropping a G-Code onto PrusaSlicer icon in Finder, + // just G-codes were passed. Switch to G-code viewer mode. + m_app_mode = EAppMode::GCodeViewer; + unlock_lockfile(get_instance_hash_string() + ".lock", data_dir() + "/cache/"); + if(app_config != nullptr) + delete app_config; + app_config = nullptr; + init_app_config(); + } + wxApp::OSXStoreOpenFiles(fileNames); +} +// wxWidgets override to get an event on open files. +void GUI_App::MacOpenFiles(const wxArrayString &fileNames) +{ + std::vector files; + std::vector gcode_files; + std::vector non_gcode_files; + for (const auto& filename : fileNames) { + if (is_gcode_file(into_u8(filename))) + gcode_files.emplace_back(filename); + else { + files.emplace_back(into_u8(filename)); + non_gcode_files.emplace_back(filename); + } + } + if (m_app_mode == EAppMode::GCodeViewer) { + // Running in G-code viewer. + // Load the first G-code into the G-code viewer. + // Or if no G-codes, send other files to slicer. + if (! gcode_files.empty()) { + if (m_post_initialized) + this->plater()->load_gcode(gcode_files.front()); + else + this->init_params->input_files = { into_u8(gcode_files.front()) }; + } + if (!non_gcode_files.empty()) + start_new_slicer(non_gcode_files, true); + } else { + if (! files.empty()) { + if (m_post_initialized) { + wxArrayString input_files; + for (size_t i = 0; i < non_gcode_files.size(); ++i) + input_files.push_back(non_gcode_files[i]); + this->plater()->load_files(input_files); + } else { + for (const auto &f : non_gcode_files) + this->init_params->input_files.emplace_back(into_u8(f)); + } + } + for (const wxString &filename : gcode_files) + start_new_gcodeviewer(&filename); + } +} +#endif /* __APPLE */ + +Sidebar& GUI_App::sidebar() +{ + return plater_->sidebar(); +} + +ObjectManipulation* GUI_App::obj_manipul() +{ + // If this method is called before plater_ has been initialized, return nullptr (to avoid a crash) + return (plater_ != nullptr) ? sidebar().obj_manipul() : nullptr; +} + +ObjectSettings* GUI_App::obj_settings() +{ + return sidebar().obj_settings(); +} + +ObjectList* GUI_App::obj_list() +{ + return sidebar().obj_list(); +} + +ObjectLayers* GUI_App::obj_layers() +{ + return sidebar().obj_layers(); +} + +Plater* GUI_App::plater() +{ + return plater_; +} + +const Plater* GUI_App::plater() const +{ + return plater_; +} + +Model& GUI_App::model() +{ + return plater_->model(); +} +wxBookCtrlBase* GUI_App::tab_panel() const +{ + return mainframe->m_tabpanel; +} + +NotificationManager * GUI_App::notification_manager() +{ + return plater_->get_notification_manager(); +} + +GalleryDialog* GUI_App::gallery_dialog() +{ + return mainframe->gallery_dialog(); +} + +// extruders count from selected printer preset +int GUI_App::extruders_cnt() const +{ + const Preset& preset = preset_bundle->printers.get_selected_preset(); + return preset.printer_technology() == ptSLA ? 1 : + preset.config.option("nozzle_diameter")->values.size(); +} + +// extruders count from edited printer preset +int GUI_App::extruders_edited_cnt() const +{ + const Preset& preset = preset_bundle->printers.get_edited_preset(); + return preset.printer_technology() == ptSLA ? 1 : + preset.config.option("nozzle_diameter")->values.size(); +} + +wxString GUI_App::current_language_code_safe() const +{ + // Translate the language code to a code, for which Prusa Research maintains translations. + const std::map mapping { + { "cs", "cs_CZ", }, + { "sk", "cs_CZ", }, + { "de", "de_DE", }, + { "es", "es_ES", }, + { "fr", "fr_FR", }, + { "it", "it_IT", }, + { "ja", "ja_JP", }, + { "ko", "ko_KR", }, + { "pl", "pl_PL", }, + { "uk", "uk_UA", }, + { "zh", "zh_CN", }, + { "ru", "ru_RU", }, + }; + wxString language_code = this->current_language_code().BeforeFirst('_'); + auto it = mapping.find(language_code); + if (it != mapping.end()) + language_code = it->second; + else + language_code = "en_US"; + return language_code; +} + +void GUI_App::open_web_page_localized(const std::string &http_address) +{ + open_browser_with_warning_dialog(http_address + "&lng=" + this->current_language_code_safe(), nullptr, false); +} + +// If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have a part(s). +// Because of we can't to print the multi-part objects with SLA technology. +bool GUI_App::may_switch_to_SLA_preset(const wxString& caption) +{ + if (model_has_multi_part_objects(model())) { + show_info(nullptr, + _L("It's impossible to print multi-part object(s) with SLA technology.") + "\n\n" + + _L("Please check your object list before preset changing."), + caption); + return false; + } + if (model_has_connectors(model())) { + show_info(nullptr, + _L("SLA technology doesn't support cut with connectors") + "\n\n" + + _L("Please check your object list before preset changing."), + caption); + return false; + } + return true; +} + +bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page) +{ + wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + + if (reason == ConfigWizard::RR_USER) { + if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED) + return false; + } + + auto wizard = new ConfigWizard(mainframe); + const bool res = wizard->run(reason, start_page); + + if (res) { + load_current_presets(); + + // #ysFIXME - delete after testing: This part of code looks redundant. All checks are inside ConfigWizard::priv::apply_config() + if (preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) + may_switch_to_SLA_preset(_L("Configuration is editing from ConfigWizard")); + } + + return res; +} + +void GUI_App::show_desktop_integration_dialog() +{ +#ifdef __linux__ + //wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + DesktopIntegrationDialog dialog(mainframe); + dialog.ShowModal(); +#endif //__linux__ +} + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG +void GUI_App::gcode_thumbnails_debug() +{ + const std::string BEGIN_MASK = "; thumbnail begin"; + const std::string END_MASK = "; thumbnail end"; + std::string gcode_line; + bool reading_image = false; + unsigned int width = 0; + unsigned int height = 0; + + wxFileDialog dialog(GetTopWindow(), _L("Select a gcode file:"), "", "", "G-code files (*.gcode)|*.gcode;*.GCODE;", wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + std::string in_filename = into_u8(dialog.GetPath()); + std::string out_path = boost::filesystem::path(in_filename).remove_filename().append(L"thumbnail").string(); + + boost::nowide::ifstream in_file(in_filename.c_str()); + std::vector rows; + std::string row; + if (in_file.good()) + { + while (std::getline(in_file, gcode_line)) + { + if (in_file.good()) + { + if (boost::starts_with(gcode_line, BEGIN_MASK)) + { + reading_image = true; + gcode_line = gcode_line.substr(BEGIN_MASK.length() + 1); + std::string::size_type x_pos = gcode_line.find('x'); + std::string width_str = gcode_line.substr(0, x_pos); + width = (unsigned int)::atoi(width_str.c_str()); + std::string height_str = gcode_line.substr(x_pos + 1); + height = (unsigned int)::atoi(height_str.c_str()); + row.clear(); + } + else if (reading_image && boost::starts_with(gcode_line, END_MASK)) + { + std::string out_filename = out_path + std::to_string(width) + "x" + std::to_string(height) + ".png"; + boost::nowide::ofstream out_file(out_filename.c_str(), std::ios::binary); + if (out_file.good()) + { + std::string decoded; + decoded.resize(boost::beast::detail::base64::decoded_size(row.size())); + decoded.resize(boost::beast::detail::base64::decode((void*)&decoded[0], row.data(), row.size()).first); + + out_file.write(decoded.c_str(), decoded.size()); + out_file.close(); + } + + reading_image = false; + width = 0; + height = 0; + rows.clear(); + } + else if (reading_image) + row += gcode_line.substr(2); + } + } + + in_file.close(); + } +} +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + +void GUI_App::window_pos_save(wxTopLevelWindow* window, const std::string &name) +{ + if (name.empty()) { return; } + const auto config_key = (boost::format("window_%1%") % name).str(); + + WindowMetrics metrics = WindowMetrics::from_window(window); + app_config->set(config_key, metrics.serialize()); + app_config->save(); +} + +void GUI_App::window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized) +{ + if (name.empty()) { return; } + const auto config_key = (boost::format("window_%1%") % name).str(); + + if (! app_config->has(config_key)) { + window->Maximize(default_maximized); + return; + } + + auto metrics = WindowMetrics::deserialize(app_config->get(config_key)); + if (! metrics) { + window->Maximize(default_maximized); + return; + } + + const wxRect& rect = metrics->get_rect(); + + if (app_config->get("restore_win_position") == "1") { + // workaround for crash related to the positioning of the window on secondary monitor + app_config->set("restore_win_position", (boost::format("crashed_at_%1%_pos") % name).str()); + app_config->save(); + window->SetPosition(rect.GetPosition()); + + // workaround for crash related to the positioning of the window on secondary monitor + app_config->set("restore_win_position", (boost::format("crashed_at_%1%_size") % name).str()); + app_config->save(); + window->SetSize(rect.GetSize()); + + // revert "restore_win_position" value if application wasn't crashed + app_config->set("restore_win_position", "1"); + app_config->save(); + } + else + window->CenterOnScreen(); + + window->Maximize(metrics->get_maximized()); +} + +void GUI_App::window_pos_sanitize(wxTopLevelWindow* window) +{ + /*unsigned*/int display_idx = wxDisplay::GetFromWindow(window); + wxRect display; + if (display_idx == wxNOT_FOUND) { + display = wxDisplay(0u).GetClientArea(); + window->Move(display.GetTopLeft()); + } else { + display = wxDisplay(display_idx).GetClientArea(); + } + + auto metrics = WindowMetrics::from_window(window); + metrics.sanitize_for_display(display); + if (window->GetScreenRect() != metrics.get_rect()) { + window->SetSize(metrics.get_rect()); + } +} + +bool GUI_App::config_wizard_startup() +{ + if (!m_app_conf_exists || preset_bundle->printers.only_default_printers()) { + run_wizard(ConfigWizard::RR_DATA_EMPTY); + return true; + } else if (get_app_config()->legacy_datadir()) { + // Looks like user has legacy pre-vendorbundle data directory, + // explain what this is and run the wizard + + MsgDataLegacy dlg; + dlg.ShowModal(); + + run_wizard(ConfigWizard::RR_DATA_LEGACY); + return true; + } + return false; +} + +bool GUI_App::check_updates(const bool verbose) +{ + PresetUpdater::UpdateResult updater_result; + try { + updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION); + if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { + mainframe->Close(); + // Applicaiton is closing. + return false; + } + else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { + m_app_conf_exists = true; + } + else if (verbose && updater_result == PresetUpdater::R_NOOP) { + MsgNoUpdates dlg; + dlg.ShowModal(); + } + } + catch (const std::exception & ex) { + show_error(nullptr, ex.what()); + } + // Applicaiton will continue. + return true; +} + +bool GUI_App::open_browser_with_warning_dialog(const wxString& url, wxWindow* parent/* = nullptr*/, bool force_remember_choice /*= true*/, int flags/* = 0*/) +{ + bool launch = true; + + // warning dialog containes a "Remember my choice" checkbox + std::string option_key = "suppress_hyperlinks"; + if (force_remember_choice || app_config->get(option_key).empty()) { + if (app_config->get(option_key).empty()) { + RichMessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); + dialog.ShowCheckBox(_L("Remember my choice")); + auto answer = dialog.ShowModal(); + launch = answer == wxID_YES; + if (dialog.IsCheckBoxChecked()) { + wxString preferences_item = _L("Suppress to open hyperlink in browser"); + wxString msg = + _L("PrusaSlicer will remember your choice.") + "\n\n" + + _L("You will not be asked about it again on hyperlinks hovering.") + "\n\n" + + format_wxstr(_L("Visit \"Preferences\" and check \"%1%\"\nto changes your choice."), preferences_item); + + MessageDialog msg_dlg(parent, msg, _L("PrusaSlicer: Don't ask me again"), wxOK | wxCANCEL | wxICON_INFORMATION); + if (msg_dlg.ShowModal() == wxID_CANCEL) + return false; + app_config->set(option_key, answer == wxID_NO ? "1" : "0"); + } + } + if (launch) + launch = app_config->get(option_key) != "1"; + } + // warning dialog doesn't containe a "Remember my choice" checkbox + // and will be shown only when "Suppress to open hyperlink in browser" is ON. + else if (app_config->get(option_key) == "1") { + MessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); + launch = dialog.ShowModal() == wxID_YES; + } + + return launch && wxLaunchDefaultBrowser(url, flags); +} + +// static method accepting a wxWindow object as first parameter +// void warning_catcher{ +// my($self, $message_dialog) = @_; +// return sub{ +// my $message = shift; +// return if $message = ~/ GLUquadricObjPtr | Attempt to free unreferenced scalar / ; +// my @params = ($message, 'Warning', wxOK | wxICON_WARNING); +// $message_dialog +// ? $message_dialog->(@params) +// : Wx::MessageDialog->new($self, @params)->ShowModal; +// }; +// } + +// Do we need this function??? +// void GUI_App::notify(message) { +// auto frame = GetTopWindow(); +// // try harder to attract user attention on OS X +// if (!frame->IsActive()) +// frame->RequestUserAttention(defined(__WXOSX__/*&Wx::wxMAC */)? wxUSER_ATTENTION_ERROR : wxUSER_ATTENTION_INFO); +// +// // There used to be notifier using a Growl application for OSX, but Growl is dead. +// // The notifier also supported the Linux X D - bus notifications, but that support was broken. +// //TODO use wxNotificationMessage ? +// } + + +#ifdef __WXMSW__ +void GUI_App::associate_3mf_files() +{ + associate_file_type(L".3mf", L"Prusa.Slicer.1", L"PrusaSlicer", true); +} + +void GUI_App::associate_stl_files() +{ + associate_file_type(L".stl", L"Prusa.Slicer.1", L"PrusaSlicer", true); +} + +void GUI_App::associate_gcode_files() +{ + associate_file_type(L".gcode", L"PrusaSlicer.GCodeViewer.1", L"PrusaSlicerGCodeViewer", true); +} +#endif // __WXMSW__ + +void GUI_App::on_version_read(wxCommandEvent& evt) +{ + app_config->set("version_online", into_u8(evt.GetString())); + app_config->save(); + std::string opt = app_config->get("notify_release"); + if (this->plater_ == nullptr || (opt != "all" && opt != "release")) { + return; + } + if (*Semver::parse(SLIC3R_VERSION) >= *Semver::parse(into_u8(evt.GetString()))) { + return; + } + // notification + /* + this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAvailable + , NotificationManager::NotificationLevel::ImportantNotificationLevel + , Slic3r::format(_u8L("New release version %1% is available."), evt.GetString()) + , _u8L("See Download page.") + , [](wxEvtHandler* evnthndlr) {wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); return true; } + ); + */ + // updater + // read triggered_by_user that was set when calling GUI_App::app_version_check + app_updater(m_app_updater->get_triggered_by_user()); +} + +void GUI_App::app_updater(bool from_user) +{ + DownloadAppData app_data = m_app_updater->get_app_data(); + + if (from_user && (!app_data.version || *app_data.version <= *Semver::parse(SLIC3R_VERSION))) + { + BOOST_LOG_TRIVIAL(info) << "There is no newer version online."; + MsgNoAppUpdates no_update_dialog; + no_update_dialog.ShowModal(); + return; + + } + + assert(!app_data.url.empty()); + assert(!app_data.target_path.empty()); + + // dialog with new version info + AppUpdateAvailableDialog dialog(*Semver::parse(SLIC3R_VERSION), *app_data.version); + auto dialog_result = dialog.ShowModal(); + // checkbox "do not show again" + if (dialog.disable_version_check()) { + app_config->set("notify_release", "none"); + } + // Doesn't wish to update + if (dialog_result != wxID_OK) { + return; + } + // dialog with new version download (installer or app dependent on system) including path selection + AppUpdateDownloadDialog dwnld_dlg(*app_data.version, app_data.target_path); + dialog_result = dwnld_dlg.ShowModal(); + // Doesn't wish to download + if (dialog_result != wxID_OK) { + return; + } + app_data.target_path =dwnld_dlg.get_download_path(); + + // start download + this->plater_->get_notification_manager()->push_download_progress_notification(_utf8("Download"), std::bind(&AppUpdater::cancel_callback, this->m_app_updater.get())); + app_data.start_after = dwnld_dlg.run_after_download(); + m_app_updater->set_app_data(std::move(app_data)); + m_app_updater->sync_download(); +} + +void GUI_App::app_version_check(bool from_user) +{ + if (from_user) { + if (m_app_updater->get_download_ongoing()) { + MessageDialog msgdlg(nullptr, _L("Download of new version is already ongoing. Do you wish to continue?"), _L("Notice"), wxYES_NO); + if (msgdlg.ShowModal() != wxID_YES) + return; + } + } + std::string version_check_url = app_config->version_check_url(); + m_app_updater->sync_version(version_check_url, from_user); +} + +} // GUI +} //Slic3r diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 8f69ca811..6ffe19218 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -493,7 +493,9 @@ void MenuFactory::append_menu_items_add_volume(wxMenu* menu) append_menu_item(menu, wxID_ANY, _(ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::MODEL_PART)].first), "", [](wxCommandEvent&) { obj_list()->load_subobject(ModelVolumeType::MODEL_PART); }, ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::MODEL_PART)].second, nullptr, - []() { return obj_list()->is_instance_or_object_selected(); }, m_parent); + []() { return obj_list()->is_instance_or_object_selected() + && !obj_list()->is_selected_object_cut(); + }, m_parent); } if (mode == comSimple) { append_menu_item(menu, wxID_ANY, _(ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::SUPPORT_ENFORCER)].first), "", @@ -514,7 +516,10 @@ void MenuFactory::append_menu_items_add_volume(wxMenu* menu) wxMenu* sub_menu = append_submenu_add_generic(menu, ModelVolumeType(type)); append_submenu(menu, sub_menu, wxID_ANY, _(item.first), "", item.second, - []() { return obj_list()->is_instance_or_object_selected(); }, m_parent); + [type]() { + bool can_add = type < size_t(ModelVolumeType::PARAMETER_MODIFIER) ? !obj_list()->is_selected_object_cut() : true; + return can_add && obj_list()->is_instance_or_object_selected(); + }, m_parent); } append_menu_item_layers_editing(menu); @@ -680,6 +685,21 @@ wxMenuItem* MenuFactory::append_menu_item_printable(wxMenu* menu) return menu_item_printable; } +void MenuFactory::append_menu_item_invalidate_cut_info(wxMenu* menu) +{ + const wxString menu_name = _L("Invalidate cut info"); + + auto menu_item_id = menu->FindItem(menu_name); + if (menu_item_id != wxNOT_FOUND) + // Delete old menu item if selected object isn't cut + menu->Destroy(menu_item_id); + + if (obj_list()->has_selected_cut_object()) + append_menu_item(menu, wxID_ANY, menu_name, "", + [](wxCommandEvent&) { obj_list()->invalidate_cut_info_for_selection(); }, "", menu, + []() { return true; }, m_parent); +} + void MenuFactory::append_menu_items_osx(wxMenu* menu) { append_menu_item(menu, wxID_ANY, _L("Rename"), "", @@ -816,6 +836,8 @@ void MenuFactory::append_menu_items_convert_unit(wxMenu* menu, int insert_pos/* ModelObjectPtrs objects; for (int obj_idx : obj_idxs) { ModelObject* object = obj_list()->object(obj_idx); + if (object->is_cut()) + return false; if (vol_idxs.empty()) { for (ModelVolume* volume : object->volumes) if (volume_respects_conversion(volume, conver_type)) @@ -1016,6 +1038,7 @@ wxMenu* MenuFactory::object_menu() append_menu_item_settings(&m_object_menu); append_menu_item_change_extruder(&m_object_menu); update_menu_items_instance_manipulation(mtObjectFFF); + append_menu_item_invalidate_cut_info(&m_object_menu); return &m_object_menu; } @@ -1025,6 +1048,7 @@ wxMenu* MenuFactory::sla_object_menu() append_menu_items_convert_unit(&m_sla_object_menu, 11); append_menu_item_settings(&m_sla_object_menu); update_menu_items_instance_manipulation(mtObjectSLA); + append_menu_item_invalidate_cut_info(&m_sla_object_menu); return &m_sla_object_menu; } @@ -1056,6 +1080,9 @@ wxMenu* MenuFactory::multi_selection_menu() wxDataViewItemArray sels; obj_list()->GetSelections(sels); + if (sels.IsEmpty()) + return nullptr; + for (const wxDataViewItem& item : sels) if (!(list_model()->GetItemType(item) & (itVolume | itObject | itInstance))) // show this menu only for Objects(Instances mixed with Objects)/Volumes selection diff --git a/src/slic3r/GUI/GUI_Factories.hpp b/src/slic3r/GUI/GUI_Factories.hpp index bbbc00d42..7190edb64 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -89,6 +89,7 @@ private: wxMenuItem* append_menu_item_change_type(wxMenu* menu); wxMenuItem* append_menu_item_instance_to_object(wxMenu* menu); wxMenuItem* append_menu_item_printable(wxMenu* menu); + void append_menu_item_invalidate_cut_info(wxMenu *menu); void append_menu_items_osx(wxMenu* menu); wxMenuItem* append_menu_item_fix_through_netfabb(wxMenu* menu); wxMenuItem* append_menu_item_simplify(wxMenu* menu); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 0984ded3a..164679645 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -11,6 +11,8 @@ #include "GalleryDialog.hpp" #include "MainFrame.hpp" #include "slic3r/Utils/UndoRedo.hpp" +#include "Gizmos/GLGizmoCut.hpp" +#include "Gizmos/GLGizmoScale.hpp" #include "OptionsGroup.hpp" #include "Tab.hpp" @@ -401,6 +403,13 @@ MeshErrorsInfo ObjectList::get_mesh_errors_info(const int obj_idx, const int vol if (obj_idx < 0) return { {}, {} }; // hide tooltip + const ModelObject* object = (*m_objects)[obj_idx]; + if (vol_idx != -1 && vol_idx >= int(object->volumes.size())) { + if (sidebar_info) + *sidebar_info = _L("Wrong volume index "); + return { {}, {} }; // hide tooltip + } + const TriangleMeshStats& stats = vol_idx == -1 ? (*m_objects)[obj_idx]->get_object_stl_stats() : (*m_objects)[obj_idx]->volumes[vol_idx]->mesh().stats(); @@ -673,6 +682,8 @@ void ObjectList::selection_changed() fix_multiselection_conflicts(); + fix_cut_selection(); + // update object selection on Plater if (!m_prevent_canvas_selection_update) update_selections_on_canvas(); @@ -1389,6 +1400,15 @@ bool ObjectList::is_instance_or_object_selected() return selection.is_single_full_instance() || selection.is_single_full_object(); } +bool ObjectList::is_selected_object_cut() +{ + const Selection& selection = scene_selection(); + int obj_idx = selection.get_object_idx(); + if (obj_idx < 0) + return false; + return object(obj_idx)->is_cut(); +} + void ObjectList::load_subobject(ModelVolumeType type, bool from_galery/* = false*/) { if (type == ModelVolumeType::INVALID && from_galery) { @@ -1708,6 +1728,9 @@ void ObjectList::load_generic_subobject(const std::string& type_name, const Mode // update printable state on canvas wxGetApp().plater()->canvas3D()->update_instance_printable_state_for_object((size_t)obj_idx); + if (model_object.is_cut()) + update_info_items(obj_idx); + selection_changed(); } @@ -1804,22 +1827,22 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name #endif /* _DEBUG */ } -void ObjectList::del_object(const int obj_idx) +bool ObjectList::del_object(const int obj_idx) { - wxGetApp().plater()->delete_object_from_model(obj_idx); + return wxGetApp().plater()->delete_object_from_model(obj_idx); } // Delete subobject -void ObjectList::del_subobject_item(wxDataViewItem& item) +bool ObjectList::del_subobject_item(wxDataViewItem& item) { - if (!item) return; + if (!item) return false; int obj_idx, idx; ItemType type; m_objects_model->GetItemInfo(item, type, obj_idx, idx); if (type == itUndef) - return; + return false; wxDataViewItem parent = m_objects_model->GetParent(item); @@ -1833,19 +1856,21 @@ void ObjectList::del_subobject_item(wxDataViewItem& item) del_layer_from_object(obj_idx, m_objects_model->GetLayerRangeByItem(item)); else if (type & itInfo && obj_idx != -1) del_info_item(obj_idx, m_objects_model->GetInfoItemType(item)); - else if (idx == -1) - return; - else if (!del_subobject_from_object(obj_idx, idx, type)) - return; + else if (idx == -1 || !del_subobject_from_object(obj_idx, idx, type)) + return false; // If last volume item with warning was deleted, unmark object item if (type & itVolume) { + add_volumes_to_object_in_list(obj_idx); const std::string& icon_name = get_warning_icon_name(object(obj_idx)->get_object_stl_stats()); m_objects_model->UpdateWarningIcon(parent, icon_name); } + else + m_objects_model->Delete(item); - m_objects_model->Delete(item); update_info_items(obj_idx); + + return true; } void ObjectList::del_info_item(const int obj_idx, InfoItemType type) @@ -1868,6 +1893,10 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type) mv->seam_facets.reset(); break; + case InfoItemType::CutConnectors: + show_error(nullptr, _L("Connectors cannot be deleted from cut object.")); + break; + case InfoItemType::MmuSegmentation: cnv->get_gizmos_manager().reset_all_states(); Plater::TakeSnapshot(plater, _L("Remove Multi Material painting")); @@ -1978,6 +2007,16 @@ bool ObjectList::del_subobject_from_object(const int obj_idx, const int idx, con Slic3r::GUI::show_error(nullptr, _L("From Object List You can't delete the last solid part from object.")); return false; } + if (object->is_cut()) { + if (volume->is_model_part()) { + Slic3r::GUI::show_error(nullptr, _L("Solid part cannot be deleted from cut object.")); + return false; + } + if (volume->is_negative_volume()) { + Slic3r::GUI::show_error(nullptr, _L("Negative volume cannot be deleted from cut object.")); + return false; + } + } take_snapshot(_L("Delete Subobject")); @@ -2005,6 +2044,10 @@ bool ObjectList::del_subobject_from_object(const int obj_idx, const int idx, con Slic3r::GUI::show_error(nullptr, _L("Last instance of an object cannot be deleted.")); return false; } + if (object->is_cut()) { + Slic3r::GUI::show_error(nullptr, _L("Instance cannot be deleted from cut object.")); + return false; + } take_snapshot(_L("Delete Instance")); object->delete_instance(idx); @@ -2038,30 +2081,11 @@ void ObjectList::split() volume->split(nozzle_dmrs_cnt); + (*m_objects)[obj_idx]->input_file.clear(); + wxBusyCursor wait; - auto model_object = (*m_objects)[obj_idx]; - - auto parent = m_objects_model->GetTopParent(item); - if (parent) - m_objects_model->DeleteVolumeChildren(parent); - else - parent = item; - - for (const ModelVolume* volume : model_object->volumes) { - const wxDataViewItem& vol_item = m_objects_model->AddVolumeChild(parent, from_u8(volume->name), - volume->type(),// is_modifier() ? ModelVolumeType::PARAMETER_MODIFIER : ModelVolumeType::MODEL_PART, - get_warning_icon_name(volume->mesh().stats()), - volume->config.has("extruder") ? volume->config.extruder() : 0, - false); - // add settings to the part, if it has those - add_settings_item(vol_item, &volume->config.get()); - } - - model_object->input_file.clear(); - - if (parent == item) - Expand(parent); + add_volumes_to_object_in_list(obj_idx); changed_object(obj_idx); // update printable state for new volumes on canvas3D @@ -2380,9 +2404,12 @@ bool ObjectList::is_splittable(bool to_objects) auto obj_idx = get_selected_obj_idx(); if (obj_idx < 0) return false; - if ((*m_objects)[obj_idx]->volumes.size() > 1) + const ModelObject* object = (*m_objects)[obj_idx]; + if (object->is_cut()) + return false; + if (object->volumes.size() > 1) return true; - return (*m_objects)[obj_idx]->volumes[0]->is_splittable(); + return object->volumes[0]->is_splittable(); } return false; } @@ -2415,9 +2442,59 @@ bool ObjectList::can_split_instances() return selection.is_multiple_full_instance() || selection.is_single_full_instance(); } +bool ObjectList::has_selected_cut_object() const +{ + wxDataViewItemArray sels; + GetSelections(sels); + if (sels.IsEmpty()) + return false; + + for (wxDataViewItem item : sels) { + const int obj_idx = m_objects_model->GetObjectIdByItem(item); + if (obj_idx >= 0 && object(obj_idx)->is_cut()) + return true; + } + + return false; +} +void ObjectList::invalidate_cut_info_for_selection() +{ + const wxDataViewItem item = GetSelection(); + if (item) { + const int obj_idx = m_objects_model->GetObjectIdByItem(item); + if (obj_idx >= 0) + invalidate_cut_info_for_object(size_t(obj_idx)); + } +} + +void ObjectList::invalidate_cut_info_for_object(size_t obj_idx) +{ + ModelObject* init_obj = object(int(obj_idx)); + if (!init_obj->is_cut()) + return; + + take_snapshot(_L("Invalidate cut info")); + + auto invalidate_cut = [this](size_t obj_idx) { + object(int(obj_idx))->invalidate_cut(); + update_info_items(obj_idx); + add_volumes_to_object_in_list(obj_idx); + }; + + // invalidate cut for related objects (which have the same cut_id) + for (size_t idx = 0; idx < m_objects->size(); idx++) + if (ModelObject* obj = object(idx); obj != init_obj && obj->cut_id.is_equal(init_obj->cut_id)) + invalidate_cut(idx); + + // invalidate own cut information + invalidate_cut(size_t(obj_idx)); + + update_lock_icons_for_model(); +} + bool ObjectList::can_merge_to_multipart_object() const { - if (printer_technology() == ptSLA) + if (printer_technology() == ptSLA || has_selected_cut_object()) return false; wxDataViewItemArray sels; @@ -2445,17 +2522,7 @@ bool ObjectList::can_merge_to_single_object() const wxPoint ObjectList::get_mouse_position_in_control() const { - wxPoint pt = wxGetMousePosition() - this->GetScreenPosition(); - -#ifdef __APPLE__ - // Workaround for OSX. From wxWidgets 3.1.6 Hittest doesn't respect to the header of wxDataViewCtrl - if (wxDataViewItem top_item = this->GetTopItem(); top_item.IsOk()) { - auto rect = this->GetItemRect(top_item, this->GetColumn(0)); - pt.y -= rect.y; - } -#endif // __APPLE__ - - return pt; + return wxGetMousePosition() - this->GetScreenPosition(); } // NO_PARAMETERS function call means that changed object index will be determine from Selection() @@ -2476,14 +2543,65 @@ void ObjectList::part_selection_changed() bool update_and_show_settings = false; bool update_and_show_layers = false; + bool enable_manipulation {true}; + bool disable_ss_manipulation {false}; + bool disable_ununiform_scale {false}; + const auto item = GetSelection(); - if ( multiple_selection() || (item && m_objects_model->GetItemType(item) == itInstanceRoot )) { + GLGizmosManager& gizmos_mgr = wxGetApp().plater()->canvas3D()->get_gizmos_manager(); + + if (item && m_objects_model->GetItemType(item) == itInfo && m_objects_model->GetInfoItemType(item) == InfoItemType::CutConnectors) { + og_name = _L("Cut Connectors information"); + + update_and_show_manipulations = true; + enable_manipulation = false; + disable_ununiform_scale = true; + } + else if ( multiple_selection() || (item && m_objects_model->GetItemType(item) == itInstanceRoot )) { og_name = _L("Group manipulation"); const Selection& selection = scene_selection(); // don't show manipulation panel for case of all Object's parts selection update_and_show_manipulations = !selection.is_single_full_instance(); + + if (int obj_idx = selection.get_object_idx(); obj_idx >= 0) { + if (selection.is_any_volume() || selection.is_any_modifier()) + enable_manipulation = !(*m_objects)[obj_idx]->is_cut(); + else// if (item && m_objects_model->GetItemType(item) == itInstanceRoot) + disable_ss_manipulation = (*m_objects)[obj_idx]->is_cut(); + } + else { + wxDataViewItemArray sels; + GetSelections(sels); + if (selection.is_single_full_object() || selection.is_multiple_full_instance() ) { + int obj_idx = m_objects_model->GetObjectIdByItem(sels.front()); + disable_ss_manipulation = (*m_objects)[obj_idx]->is_cut(); + } + else if (selection.is_mixed() || selection.is_multiple_full_object()) { + std::map> cut_objects; + + // find cut objects + for (auto item : sels) { + int obj_idx = m_objects_model->GetObjectIdByItem(item); + const ModelObject* obj = object(obj_idx); + if (obj->is_cut()) { + if (cut_objects.find(obj->cut_id) == cut_objects.end()) + cut_objects[obj->cut_id] = std::set{ obj_idx }; + else + cut_objects.at(obj->cut_id).insert(obj_idx); + } + } + + // check if selected cut objects are "full selected" + for (auto cut_object : cut_objects) + if (cut_object.first.check_sum() != cut_object.second.size()) { + disable_ss_manipulation = true; + break; + } + disable_ununiform_scale = !cut_objects.empty(); + } + } } else { if (item) { @@ -2491,11 +2609,12 @@ void ObjectList::part_selection_changed() const wxDataViewItem parent = m_objects_model->GetParent(item); const ItemType parent_type = m_objects_model->GetItemType(parent); obj_idx = m_objects_model->GetObjectIdByItem(item); + ModelObject* object = (*m_objects)[obj_idx]; if (parent == wxDataViewItem(nullptr) || type == itInfo) { og_name = _L("Object manipulation"); - m_config = &(*m_objects)[obj_idx]->config; + m_config = &object->config; update_and_show_manipulations = true; if (type == itInfo) { @@ -2511,29 +2630,30 @@ void ObjectList::part_selection_changed() case InfoItemType::CustomSeam: case InfoItemType::MmuSegmentation: { - GLGizmosManager::EType gizmo_type = info_type == InfoItemType::CustomSupports ? GLGizmosManager::EType::FdmSupports : - info_type == InfoItemType::CustomSeam ? GLGizmosManager::EType::Seam : + GLGizmosManager::EType gizmo_type = info_type == InfoItemType::CustomSupports ? GLGizmosManager::EType::FdmSupports : + info_type == InfoItemType::CustomSeam ? GLGizmosManager::EType::Seam : GLGizmosManager::EType::MmuSegmentation; - GLGizmosManager& gizmos_mgr = wxGetApp().plater()->canvas3D()->get_gizmos_manager(); if (gizmos_mgr.get_current_type() != gizmo_type) gizmos_mgr.open_gizmo(gizmo_type); break; } - case InfoItemType::Sinking: { break; } + case InfoItemType::Sinking: default: { break; } } } + else + disable_ss_manipulation = object->is_cut(); } else { if (type & itSettings) { if (parent_type & itObject) { og_name = _L("Object Settings to modify"); - m_config = &(*m_objects)[obj_idx]->config; + m_config = &object->config; } else if (parent_type & itVolume) { og_name = _L("Part Settings to modify"); volume_id = m_objects_model->GetVolumeIdByItem(parent); - m_config = &(*m_objects)[obj_idx]->volumes[volume_id]->config; + m_config = &object->volumes[volume_id]->config; } else if (parent_type & itLayer) { og_name = _L("Layer range Settings to modify"); @@ -2544,15 +2664,18 @@ void ObjectList::part_selection_changed() else if (type & itVolume) { og_name = _L("Part manipulation"); volume_id = m_objects_model->GetVolumeIdByItem(item); - m_config = &(*m_objects)[obj_idx]->volumes[volume_id]->config; + m_config = &object->volumes[volume_id]->config; update_and_show_manipulations = true; + const ModelVolume* volume = object->volumes[volume_id]; + enable_manipulation = !(object->is_cut() && (volume->is_cut_connector() || volume->is_model_part())); } else if (type & itInstance) { og_name = _L("Instance manipulation"); update_and_show_manipulations = true; // fill m_config by object's values - m_config = &(*m_objects)[obj_idx]->config; + m_config = &object->config; + disable_ss_manipulation = object->is_cut(); } else if (type & (itLayerRoot|itLayer)) { og_name = type & itLayerRoot ? _L("Height ranges") : _L("Settings for height range"); @@ -2571,10 +2694,20 @@ void ObjectList::part_selection_changed() wxGetApp().obj_manipul()->get_og()->set_name(" " + og_name + " "); if (item) { - // wxGetApp().obj_manipul()->get_og()->set_value("object_name", m_objects_model->GetName(item)); wxGetApp().obj_manipul()->update_item_name(m_objects_model->GetName(item)); wxGetApp().obj_manipul()->update_warning_icon_state(get_mesh_errors_info(obj_idx, volume_id)); } + + if (disable_ss_manipulation) + wxGetApp().obj_manipul()->DisableScale(); + else { + wxGetApp().obj_manipul()->Enable(enable_manipulation); + if (disable_ununiform_scale) + wxGetApp().obj_manipul()->DisableUnuniformScale(); + } + + if (GLGizmoScale3D* scale = dynamic_cast(gizmos_mgr.get_gizmo(GLGizmosManager::Scale))) + scale->enable_ununiversal_scale(!disable_ununiform_scale); } if (update_and_show_settings) @@ -2597,6 +2730,7 @@ void ObjectList::part_selection_changed() #else wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event("", false); #endif // ENABLE_WORLD_COORDINATE + wxGetApp().plater()->canvas3D()->enable_moving(enable_manipulation); // ysFIXME wxGetApp().obj_manipul() ->UpdateAndShow(update_and_show_manipulations); wxGetApp().obj_settings()->UpdateAndShow(update_and_show_settings); wxGetApp().obj_layers() ->UpdateAndShow(update_and_show_layers); @@ -2655,6 +2789,7 @@ void ObjectList::update_info_items(size_t obj_idx, wxDataViewItemArray* selectio for (InfoItemType type : {InfoItemType::CustomSupports, InfoItemType::CustomSeam, + InfoItemType::CutConnectors, InfoItemType::MmuSegmentation, InfoItemType::Sinking, InfoItemType::VariableLayerHeight}) { @@ -2675,6 +2810,9 @@ void ObjectList::update_info_items(size_t obj_idx, wxDataViewItemArray* selectio }); break; + case InfoItemType::CutConnectors: + should_show = model_object->is_cut() && model_object->has_connectors() && model_object->volumes.size() > 1; + break; case InfoItemType::VariableLayerHeight : should_show = printer_technology() == ptFFF && ! model_object->layer_height_profile.empty(); @@ -2714,31 +2852,73 @@ void ObjectList::update_info_items(size_t obj_idx, wxDataViewItemArray* selectio } } +static wxString extruder2str(int extruder) +{ + return extruder == 0 ? _L("default") : wxString::Format("%d", extruder); +} +static bool can_add_volumes_to_object(const ModelObject* object) +{ + bool can = object->volumes.size() > 1; + + if (can && object->is_cut()) { + int no_connectors_cnt = 0; + for (const ModelVolume* v : object->volumes) + if (!v->is_cut_connector()) { + if (!v->is_model_part()) + return true; + no_connectors_cnt++; + } + can = no_connectors_cnt > 1; + } + + return can; +} + +wxDataViewItemArray ObjectList::add_volumes_to_object_in_list(size_t obj_idx, std::function add_to_selection/* = nullptr*/) +{ + wxDataViewItem object_item = m_objects_model->GetItemById(int(obj_idx)); + m_objects_model->DeleteVolumeChildren(object_item); + + wxDataViewItemArray items; + + const ModelObject* object = (*m_objects)[obj_idx]; + // add volumes to the object + if (can_add_volumes_to_object(object)) { + int volume_idx{ -1 }; + for (const ModelVolume* volume : object->volumes) { + ++volume_idx; + if (object->is_cut() && volume->is_cut_connector()) + continue; + const wxDataViewItem& vol_item = m_objects_model->AddVolumeChild(object_item, + from_u8(volume->name), + volume_idx, + volume->type(), + get_warning_icon_name(volume->mesh().stats()), + extruder2str(volume->config.has("extruder") ? volume->config.extruder() : 0)); + add_settings_item(vol_item, &volume->config.get()); + + if (add_to_selection && add_to_selection(volume)) + items.Add(vol_item); + } + Expand(object_item); + } + + return items; +} void ObjectList::add_object_to_list(size_t obj_idx, bool call_selection_changed) { auto model_object = (*m_objects)[obj_idx]; const wxString& item_name = from_u8(model_object->name); - const auto item = m_objects_model->Add(item_name, - model_object->config.has("extruder") ? model_object->config.extruder() : 0, - get_warning_icon_name(model_object->mesh().stats())); + const auto item = m_objects_model->AddObject(item_name, + extruder2str(model_object->config.has("extruder") ? model_object->config.extruder() : 0), + get_warning_icon_name(model_object->mesh().stats()), + model_object->is_cut()); update_info_items(obj_idx, nullptr, call_selection_changed); - // add volumes to the object - if (model_object->volumes.size() > 1) { - for (const ModelVolume* volume : model_object->volumes) { - const wxDataViewItem& vol_item = m_objects_model->AddVolumeChild(item, - from_u8(volume->name), - volume->type(), - get_warning_icon_name(volume->mesh().stats()), - volume->config.has("extruder") ? volume->config.extruder() : 0, - false); - add_settings_item(vol_item, &volume->config.get()); - } - Expand(item); - } + add_volumes_to_object_in_list(obj_idx); // add instances to the object, if it has those if (model_object->instances.size()>1) @@ -2792,29 +2972,39 @@ void ObjectList::delete_instance_from_list(const size_t obj_idx, const size_t in select_item([this, obj_idx, inst_idx]() { return m_objects_model->Delete(m_objects_model->GetItemByInstanceId(obj_idx, inst_idx)); }); } -void ObjectList::delete_from_model_and_list(const ItemType type, const int obj_idx, const int sub_obj_idx) +void ObjectList::update_lock_icons_for_model() { - if ( !(type&(itObject|itVolume|itInstance)) ) - return; - - take_snapshot(_(L("Delete Selected Item"))); - - if (type&itObject) { - del_object(obj_idx); - delete_object_from_list(obj_idx); - } - else { - del_subobject_from_object(obj_idx, sub_obj_idx, type); - - type == itVolume ? delete_volume_from_list(obj_idx, sub_obj_idx) : - delete_instance_from_list(obj_idx, sub_obj_idx); - } + for (size_t obj_idx = 0; obj_idx < (*m_objects).size(); ++obj_idx) + if (!(*m_objects)[obj_idx]->is_cut()) + m_objects_model->UpdateLockIcon(m_objects_model->GetItemById(int(obj_idx)), false); } -void ObjectList::delete_from_model_and_list(const std::vector& items_for_delete) +bool ObjectList::delete_from_model_and_list(const ItemType type, const int obj_idx, const int sub_obj_idx) +{ + if (type & (itObject | itVolume | itInstance)) { + if (type & itObject) { + bool was_cut = object(obj_idx)->is_cut(); + if (del_object(obj_idx)) { + delete_object_from_list(obj_idx); + if (was_cut) + update_lock_icons_for_model(); + return true; + } + return false; + } + if (del_subobject_from_object(obj_idx, sub_obj_idx, type)) { + type == itVolume ? delete_volume_from_list(obj_idx, sub_obj_idx) : + delete_instance_from_list(obj_idx, sub_obj_idx); + return true; + } + } + return false; +} + +bool ObjectList::delete_from_model_and_list(const std::vector& items_for_delete) { if (items_for_delete.empty()) - return; + return false; m_prevent_list_events = true; @@ -2823,14 +3013,18 @@ void ObjectList::delete_from_model_and_list(const std::vector& it if (!(item->type&(itObject | itVolume | itInstance))) continue; if (item->type&itObject) { - del_object(item->obj_idx); + bool was_cut = object(item->obj_idx)->is_cut(); + if (!del_object(item->obj_idx)) + return false;// continue; m_objects_model->Delete(m_objects_model->GetItemById(item->obj_idx)); + if (was_cut) + update_lock_icons_for_model(); } else { if (!del_subobject_from_object(item->obj_idx, item->sub_obj_idx, item->type)) continue; if (item->type&itVolume) { - m_objects_model->Delete(m_objects_model->GetItemByVolumeId(item->obj_idx, item->sub_obj_idx)); + add_volumes_to_object_in_list(item->obj_idx); ModelObject* obj = object(item->obj_idx); if (obj->volumes.size() == 1) { wxDataViewItem parent = m_objects_model->GetItemById(item->obj_idx); @@ -2854,8 +3048,12 @@ void ObjectList::delete_from_model_and_list(const std::vector& it update_info_items(id); } - m_prevent_list_events = true; + m_prevent_list_events = false; + if (modified_objects_ids.empty()) + return false; part_selection_changed(); + + return true; } void ObjectList::delete_all_objects_from_list() @@ -2960,8 +3158,10 @@ void ObjectList::remove() { wxDataViewItem parent = m_objects_model->GetParent(item); ItemType type = m_objects_model->GetItemType(item); - if (type & itObject) - delete_from_model_and_list(itObject, m_objects_model->GetIdByItem(item), -1); + if (type & itObject) { + if (!delete_from_model_and_list(itObject, m_objects_model->GetIdByItem(item), -1)) + return item; + } else { if (type & (itLayer | itInstance)) { // In case there is just one layer or two instances and we delete it, del_subobject_item will @@ -2971,7 +3171,8 @@ void ObjectList::remove() parent = m_objects_model->GetTopParent(item); } - del_subobject_item(item); + if (!del_subobject_item(item)) + return item; } return parent; @@ -2997,6 +3198,8 @@ void ObjectList::remove() if (m_objects_model->InvalidItem(item)) // item can be deleted for this moment (like last 2 Instances or Volumes) continue; parent = delete_item(item); + if (parent == item && m_objects_model->GetItemType(item) & itObject) // Object wasn't deleted + break; } } @@ -3189,7 +3392,7 @@ void ObjectList::add_layer_item(const t_layer_height_range& range, const auto layer_item = m_objects_model->AddLayersChild(layers_item, range, - config.opt_int("extruder"), + extruder2str(config.opt_int("extruder")), layer_idx); add_settings_item(layer_item, &config); } @@ -3283,6 +3486,24 @@ bool ObjectList::is_selected(const ItemType type) const return false; } +bool ObjectList::is_connectors_item_selected() const +{ + const wxDataViewItem& item = GetSelection(); + if (item) + return m_objects_model->GetItemType(item) == itInfo && m_objects_model->GetInfoItemType(item) == InfoItemType::CutConnectors; + + return false; +} + +bool ObjectList::is_connectors_item_selected(const wxDataViewItemArray& sels) const +{ + for (auto item : sels) + if (m_objects_model->GetItemType(item) == itInfo && m_objects_model->GetInfoItemType(item) == InfoItemType::CutConnectors) + return true; + + return false; +} + int ObjectList::get_selected_layers_range_idx() const { const wxDataViewItem& item = GetSelection(); @@ -3409,11 +3630,18 @@ void ObjectList::update_selections() else { for (auto idx : selection.get_volume_idxs()) { const auto gl_vol = selection.get_volume(idx); - if (gl_vol->volume_idx() >= 0) + if (gl_vol->volume_idx() >= 0) { // Only add GLVolumes with non-negative volume_ids. GLVolumes with negative volume ids // are not associated with ModelVolumes, but they are temporarily generated by the backend // (for example, SLA supports or SLA pad). - sels.Add(m_objects_model->GetItemByVolumeId(gl_vol->object_idx(), gl_vol->volume_idx())); + int obj_idx = gl_vol->object_idx(); + int vol_idx = gl_vol->volume_idx(); + assert(obj_idx >= 0 && vol_idx >= 0); + if (object(obj_idx)->volumes[vol_idx]->is_cut_connector()) + sels.Add(m_objects_model->GetInfoItemByType(m_objects_model->GetItemById(obj_idx), InfoItemType::CutConnectors)); + else + sels.Add(m_objects_model->GetItemByVolumeId(obj_idx, vol_idx)); + } } m_selection_mode = smVolume; } } @@ -3463,11 +3691,34 @@ void ObjectList::update_selections() if (sels.size() == 0 || m_selection_mode & smSettings) m_selection_mode = smUndef; - - select_items(sels); - // Scroll selected Item in the middle of an object list - ensure_current_item_visible(); + if (fix_cut_selection(sels) || is_connectors_item_selected(sels)) { + m_prevent_list_events = true; + + // If some part is selected, unselect all items except of selected parts of the current object + UnselectAll(); + SetSelections(sels); + + m_prevent_list_events = false; + + // update object selection on Plater + if (!m_prevent_canvas_selection_update) + update_selections_on_canvas(); + + // to update the toolbar and info sizer + if (!GetSelection() || m_objects_model->GetItemType(GetSelection()) == itObject || is_connectors_item_selected()) { + auto event = SimpleEvent(EVT_OBJ_LIST_OBJECT_SELECT); + event.SetEventObject(this); + wxPostEvent(this, event); + } + part_selection_changed(); + } + else { + select_items(sels); + + // Scroll selected Item in the middle of an object list + ensure_current_item_visible(); + } } void ObjectList::update_selections_on_canvas() @@ -3501,16 +3752,29 @@ void ObjectList::update_selections_on_canvas() volume_idxs.insert(volume_idxs.end(), idxs.begin(), idxs.end()); } else if (type == itInfo) { - // When selecting an info item, select one instance of the - // respective object - a gizmo may want to be opened. - int inst_idx = selection.get_instance_idx(); - int scene_obj_idx = selection.get_object_idx(); - mode = Selection::Instance; - // select first instance, unless an instance of the object is already selected - if (scene_obj_idx == -1 || inst_idx == -1 || scene_obj_idx != obj_idx) - inst_idx = 0; - std::vector idxs = selection.get_volume_idxs_from_instance(obj_idx, inst_idx); - volume_idxs.insert(volume_idxs.end(), idxs.begin(), idxs.end()); + if (m_objects_model->GetInfoItemType(item) == InfoItemType::CutConnectors) { + mode = Selection::Volume; + + // When selecting CutConnectors info item, select all object volumes, which are marked as a connector + const ModelObject* obj = object(obj_idx); + for (unsigned int vol_idx = 0; vol_idx < obj->volumes.size(); vol_idx++) + if (obj->volumes[vol_idx]->is_cut_connector()) { + std::vector idxs = selection.get_volume_idxs_from_volume(obj_idx, std::max(instance_idx, 0), vol_idx); + volume_idxs.insert(volume_idxs.end(), idxs.begin(), idxs.end()); + } + } + else { + // When selecting an info item, select one instance of the + // respective object - a gizmo may want to be opened. + int inst_idx = selection.get_instance_idx(); + int scene_obj_idx = selection.get_object_idx(); + mode = Selection::Instance; + // select first instance, unless an instance of the object is already selected + if (scene_obj_idx == -1 || inst_idx == -1 || scene_obj_idx != obj_idx) + inst_idx = 0; + std::vector idxs = selection.get_volume_idxs_from_instance(obj_idx, inst_idx); + volume_idxs.insert(volume_idxs.end(), idxs.begin(), idxs.end()); + } } else { @@ -3526,6 +3790,8 @@ void ObjectList::update_selections_on_canvas() if (sel_cnt == 1) { wxDataViewItem item = GetSelection(); + if (m_objects_model->GetInfoItemType(item) == InfoItemType::CutConnectors) + selection.remove_all(); if (m_objects_model->GetItemType(item) & (itSettings | itInstanceRoot | itLayerRoot | itLayer)) add_to_selection(m_objects_model->GetParent(item), selection, instance_idx, mode); else @@ -3796,6 +4062,53 @@ void ObjectList::fix_multiselection_conflicts() m_prevent_list_events = false; } +bool ObjectList::fix_cut_selection(wxDataViewItemArray& sels) +{ + if (wxGetApp().plater()->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Scale) { + for (const auto& item : sels) { + if (m_objects_model->GetItemType(item) & (itInstance | itObject) || + (m_objects_model->GetItemType(item) & itSettings && + m_objects_model->GetItemType(m_objects_model->GetParent(item)) & itObject)) { + + bool is_instance_selection = m_objects_model->GetItemType(item) & itInstance; + + int object_idx = m_objects_model->GetObjectIdByItem(item); + int inst_idx = is_instance_selection ? m_objects_model->GetInstanceIdByItem(item) : 0; + + if (auto obj = object(object_idx); obj->is_cut()) { + sels.Clear(); + + auto cut_id = obj->cut_id; + + int objects_cnt = int((*m_objects).size()); + for (int obj_idx = 0; obj_idx < objects_cnt; ++obj_idx) { + auto object = (*m_objects)[obj_idx]; + if (object->is_cut() && object->cut_id.has_same_id(cut_id)) + sels.Add(is_instance_selection ? m_objects_model->GetItemByInstanceId(obj_idx, inst_idx) : m_objects_model->GetItemById(obj_idx)); + } + return true; + } + } + } + } + return false; +} + +void ObjectList::fix_cut_selection() +{ + wxDataViewItemArray sels; + GetSelections(sels); + if (fix_cut_selection(sels)) { + m_prevent_list_events = true; + + // If some part is selected, unselect all items except of selected parts of the current object + UnselectAll(); + SetSelections(sels); + + m_prevent_list_events = false; + } +} + ModelVolume* ObjectList::get_selected_model_volume() { wxDataViewItem item = GetSelection(); @@ -3826,10 +4139,11 @@ void ObjectList::change_part_type() if (obj_idx < 0) return; const ModelVolumeType type = volume->type(); + const ModelObject* obj = object(obj_idx); if (type == ModelVolumeType::MODEL_PART) { int model_part_cnt = 0; - for (auto vol : (*m_objects)[obj_idx]->volumes) { + for (auto vol : obj->volumes) { if (vol->type() == ModelVolumeType::MODEL_PART) ++model_part_cnt; } @@ -3840,9 +4154,18 @@ void ObjectList::change_part_type() } } - const wxString names[] = { _L("Part"), _L("Negative Volume"), _L("Modifier"), _L("Support Blocker"), _L("Support Enforcer") }; - auto new_type = ModelVolumeType(wxGetApp().GetSingleChoiceIndex(_L("Type:"), _L("Select type of part"), wxArrayString(5, names), int(type))); + const bool is_cut_object = obj->is_cut(); + wxArrayString names; + names.Alloc(is_cut_object ? 3 : 5); + if (!is_cut_object) + for (const wxString& type : { _L("Part"), _L("Negative Volume") }) + names.Add(type); + for (const wxString& type : { _L("Modifier"), _L("Support Blocker"), _L("Support Enforcer") } ) + names.Add(type); + + const int type_shift = is_cut_object ? 2 : 0; + auto new_type = ModelVolumeType(type_shift + wxGetApp().GetSingleChoiceIndex(_L("Type:"), _L("Select type of part"), names, int(type) - type_shift)); if (new_type == type || new_type == ModelVolumeType::INVALID) return; @@ -4362,33 +4685,14 @@ void ObjectList::set_extruder_for_selected_items(const int extruder) const wxGetApp().plater()->update(); } -wxDataViewItemArray ObjectList::reorder_volumes_and_get_selection(int obj_idx, std::function add_to_selection/* = nullptr*/) +wxDataViewItemArray ObjectList::reorder_volumes_and_get_selection(size_t obj_idx, std::function add_to_selection/* = nullptr*/) { - wxDataViewItemArray items; + (*m_objects)[obj_idx]->sort_volumes(wxGetApp().app_config->get("order_volumes") == "1"); - ModelObject* object = (*m_objects)[obj_idx]; - if (object->volumes.size() <= 1) - return items; + wxDataViewItemArray items = add_volumes_to_object_in_list(obj_idx, std::move(add_to_selection)); - object->sort_volumes(wxGetApp().app_config->get("order_volumes") == "1"); + changed_object(int(obj_idx)); - wxDataViewItem object_item = m_objects_model->GetItemById(obj_idx); - m_objects_model->DeleteVolumeChildren(object_item); - - for (const ModelVolume* volume : object->volumes) { - wxDataViewItem vol_item = m_objects_model->AddVolumeChild(object_item, from_u8(volume->name), - volume->type(), - get_warning_icon_name(volume->mesh().stats()), - volume->config.has("extruder") ? volume->config.extruder() : 0, - false); - // add settings to the part, if it has those - add_settings_item(vol_item, &volume->config.get()); - - if (add_to_selection && add_to_selection(volume)) - items.Add(vol_item); - } - - changed_object(obj_idx); return items; } diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index c838203ae..eb718b48d 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -247,7 +247,7 @@ public: void add_category_to_settings_from_frequent(const std::vector& category_options, wxDataViewItem item); void show_settings(const wxDataViewItem settings_item); bool is_instance_or_object_selected(); - + bool is_selected_object_cut(); void load_subobject(ModelVolumeType type, bool from_galery = false); // ! ysFIXME - delete commented code after testing and rename "load_modifier" to something common //void load_part(ModelObject& model_object, std::vector& added_volumes, ModelVolumeType type, bool from_galery = false); @@ -257,8 +257,8 @@ public: void load_shape_object_from_gallery(); void load_shape_object_from_gallery(const wxArrayString& input_files); void load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center = true); - void del_object(const int obj_idx); - void del_subobject_item(wxDataViewItem& item); + bool del_object(const int obj_idx); + bool del_subobject_item(wxDataViewItem& item); void del_settings_from_config(const wxDataViewItem& parent_item); void del_instances_from_object(const int obj_idx); void del_layer_from_object(const int obj_idx, const t_layer_height_range& layer_range); @@ -277,6 +277,9 @@ public: bool is_splittable(bool to_objects); bool selected_instances_of_same_object(); bool can_split_instances(); + bool has_selected_cut_object() const; + void invalidate_cut_info_for_selection(); + void invalidate_cut_info_for_object(size_t obj_idx); bool can_merge_to_multipart_object() const; bool can_merge_to_single_object() const; @@ -288,6 +291,9 @@ public: void changed_object(const int obj_idx = -1) const; void part_selection_changed(); + // Add object's volumes to the list + // Return selected items, if add_to_selection is defined + wxDataViewItemArray add_volumes_to_object_in_list(size_t obj_idx, std::function add_to_selection = nullptr); // Add object to the list void add_object_to_list(size_t obj_idx, bool call_selection_changed = true); // Delete object from the list @@ -295,8 +301,9 @@ public: void delete_object_from_list(const size_t obj_idx); void delete_volume_from_list(const size_t obj_idx, const size_t vol_idx); void delete_instance_from_list(const size_t obj_idx, const size_t inst_idx); - void delete_from_model_and_list(const ItemType type, const int obj_idx, const int sub_obj_idx); - void delete_from_model_and_list(const std::vector& items_for_delete); + void update_lock_icons_for_model(); + bool delete_from_model_and_list(const ItemType type, const int obj_idx, const int sub_obj_idx); + bool delete_from_model_and_list(const std::vector& items_for_delete); // Delete all objects from the list void delete_all_objects_from_list(); // Increase instances count @@ -339,6 +346,8 @@ public: void init_objects(); bool multiple_selection() const ; bool is_selected(const ItemType type) const; + bool is_connectors_item_selected() const; + bool is_connectors_item_selected(const wxDataViewItemArray& sels) const; int get_selected_layers_range_idx() const; void set_selected_layers_range_idx(const int range_idx) { m_selected_layers_range_idx = range_idx; } void set_selection_mode(SELECTION_MODE mode) { m_selection_mode = mode; } @@ -353,6 +362,9 @@ public: bool check_last_selection(wxString& msg_str); // correct current selections to avoid of the possible conflicts void fix_multiselection_conflicts(); + // correct selection in respect to the cut_id if any exists + void fix_cut_selection(); + bool fix_cut_selection(wxDataViewItemArray& sels); ModelVolume* get_selected_model_volume(); void change_part_type(); @@ -388,7 +400,7 @@ public: void toggle_printable_state(); void set_extruder_for_selected_items(const int extruder) const ; - wxDataViewItemArray reorder_volumes_and_get_selection(int obj_idx, std::function add_to_selection = nullptr); + wxDataViewItemArray reorder_volumes_and_get_selection(size_t obj_idx, std::function add_to_selection = nullptr); void apply_volumes_order(); bool has_paint_on_segmentation(); diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index 62de19811..a2d687db0 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -576,6 +576,35 @@ void ObjectManipulation::UpdateAndShow(const bool show) OG_Settings::UpdateAndShow(show); } +void ObjectManipulation::Enable(const bool enadle) +{ + for (auto editor : m_editors) + editor->Enable(enadle); + for (wxWindow* win : std::initializer_list{ m_reset_scale_button, m_reset_rotation_button, m_drop_to_bed_button, m_check_inch, m_lock_bnt +#if ENABLE_WORLD_COORDINATE + ,m_reset_skew_button +#endif // ENABLE_WORLD_COORDINATE + }) + win->Enable(enadle); +} + +void ObjectManipulation::DisableScale() +{ + for (auto editor : m_editors) + editor->Enable(editor->has_opt_key("scale") || editor->has_opt_key("size") ? false : true); + for (wxWindow* win : std::initializer_list{ m_reset_scale_button, m_lock_bnt +#if ENABLE_WORLD_COORDINATE + ,m_reset_skew_button +#endif // ENABLE_WORLD_COORDINATE + }) + win->Enable(false); +} + +void ObjectManipulation::DisableUnuniformScale() +{ + m_lock_bnt->Enable(false); +} + void ObjectManipulation::update_ui_from_settings() { if (m_imperial_units != (wxGetApp().app_config->get("use_inches") == "1")) { @@ -734,7 +763,7 @@ void ObjectManipulation::update_settings_value(const Selection& selection) #endif // ENABLE_WORLD_COORDINATE m_new_enabled = true; } - else if (obj_list->multiple_selection() || obj_list->is_selected(itInstanceRoot)) { + else if (obj_list->is_connectors_item_selected() || obj_list->multiple_selection() || obj_list->is_selected(itInstanceRoot)) { reset_settings_value(); m_new_move_label_string = L("Translate"); m_new_rotate_label_string = L("Rotate"); diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.hpp b/src/slic3r/GUI/GUI_ObjectManipulation.hpp index e9ffe804a..9995b3e6f 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.hpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.hpp @@ -64,6 +64,8 @@ public: const std::string& get_full_opt_name() const { return m_full_opt_name; } #endif // ENABLE_WORLD_COORDINATE + bool has_opt_key(const std::string& key) { return m_opt_key == key; } + private: double get_value(); }; @@ -197,6 +199,10 @@ public: void Show(const bool show) override; bool IsShown() override; void UpdateAndShow(const bool show) override; + void Enable(const bool enadle = true); + void Disable() { Enable(false); } + void DisableScale(); + void DisableUnuniformScale(); void update_ui_from_settings(); bool use_colors() { return m_use_colors; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp index 38f72c613..6305cf917 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp @@ -1,537 +1,537 @@ -#include "GLGizmoBase.hpp" -#include "slic3r/GUI/GLCanvas3D.hpp" - -#include - -#include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/GUI_ObjectManipulation.hpp" -#if ENABLE_LEGACY_OPENGL_REMOVAL -#include "slic3r/GUI/Plater.hpp" -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -// TODO: Display tooltips quicker on Linux - -namespace Slic3r { -namespace GUI { - -const float GLGizmoBase::Grabber::SizeFactor = 0.05f; -const float GLGizmoBase::Grabber::MinHalfSize = 1.5f; -const float GLGizmoBase::Grabber::DraggingScaleFactor = 1.25f; - -#if ENABLE_RAYCAST_PICKING -PickingModel GLGizmoBase::Grabber::s_cube; -PickingModel GLGizmoBase::Grabber::s_cone; -#else -GLModel GLGizmoBase::Grabber::s_cube; -GLModel GLGizmoBase::Grabber::s_cone; -#endif // ENABLE_RAYCAST_PICKING - -GLGizmoBase::Grabber::~Grabber() -{ -#if ENABLE_RAYCAST_PICKING - if (s_cube.model.is_initialized()) - s_cube.model.reset(); - - if (s_cone.model.is_initialized()) - s_cone.model.reset(); -#else - if (s_cube.is_initialized()) - s_cube.reset(); - - if (s_cone.is_initialized()) - s_cone.reset(); -#endif // ENABLE_RAYCAST_PICKING -} - -float GLGizmoBase::Grabber::get_half_size(float size) const -{ - return std::max(size * SizeFactor, MinHalfSize); -} - -float GLGizmoBase::Grabber::get_dragging_half_size(float size) const -{ - return get_half_size(size) * DraggingScaleFactor; -} - -#if ENABLE_RAYCAST_PICKING -void GLGizmoBase::Grabber::register_raycasters_for_picking(int id) -{ - picking_id = id; - // registration will happen on next call to render() -} - -void GLGizmoBase::Grabber::unregister_raycasters_for_picking() -{ - wxGetApp().plater()->canvas3D()->remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, picking_id); - picking_id = -1; - raycasters = { nullptr }; -} -#endif // ENABLE_RAYCAST_PICKING - -#if ENABLE_RAYCAST_PICKING -void GLGizmoBase::Grabber::render(float size, const ColorRGBA& render_color) -#else -void GLGizmoBase::Grabber::render(float size, const ColorRGBA& render_color, bool picking) -#endif // ENABLE_RAYCAST_PICKING -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_current_shader(); - if (shader == nullptr) - return; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_RAYCAST_PICKING - if (!s_cube.model.is_initialized()) { -#else - if (!s_cube.is_initialized()) { -#endif // ENABLE_RAYCAST_PICKING - // This cannot be done in constructor, OpenGL is not yet - // initialized at that point (on Linux at least). - indexed_triangle_set its = its_make_cube(1.0, 1.0, 1.0); - its_translate(its, -0.5f * Vec3f::Ones()); -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_RAYCAST_PICKING - s_cube.model.init_from(its); - s_cube.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); -#else - s_cube.init_from(its); -#endif // ENABLE_RAYCAST_PICKING -#else - s_cube.init_from(its, BoundingBoxf3{ { -0.5, -0.5, -0.5 }, { 0.5, 0.5, 0.5 } }); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - -#if ENABLE_RAYCAST_PICKING - if (!s_cone.model.is_initialized()) { - indexed_triangle_set its = its_make_cone(0.375, 1.5, double(PI) / 18.0); - s_cone.model.init_from(its); - s_cone.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); - } -#else - if (!s_cone.is_initialized()) - s_cone.init_from(its_make_cone(0.375, 1.5, double(PI) / 18.0)); -#endif // ENABLE_RAYCAST_PICKING - - const float half_size = dragging ? get_dragging_half_size(size) : get_half_size(size); - -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_RAYCAST_PICKING - s_cube.model.set_color(render_color); - s_cone.model.set_color(render_color); -#else - s_cube.set_color(render_color); - s_cone.set_color(render_color); -#endif // ENABLE_RAYCAST_PICKING - - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#if ENABLE_RAYCAST_PICKING - const Transform3d& view_matrix = camera.get_view_matrix(); - const Matrix3d view_matrix_no_offset = view_matrix.matrix().block(0, 0, 3, 3); - std::vector elements_matrices(GRABBER_ELEMENTS_MAX_COUNT, Transform3d::Identity()); - elements_matrices[0] = matrix * Geometry::assemble_transform(center, angles, 2.0 * half_size * Vec3d::Ones()); - Transform3d view_model_matrix = view_matrix * elements_matrices[0]; -#else - const Transform3d& view_matrix = camera.get_view_matrix(); - const Transform3d model_matrix = matrix * Geometry::assemble_transform(center, angles, 2.0 * half_size * Vec3d::Ones()); - const Transform3d view_model_matrix = view_matrix * model_matrix; -#endif // ENABLE_RAYCAST_PICKING - - shader->set_uniform("view_model_matrix", view_model_matrix); -#if ENABLE_RAYCAST_PICKING - Matrix3d view_normal_matrix = view_matrix_no_offset * elements_matrices[0].matrix().block(0, 0, 3, 3).inverse().transpose(); -#else - const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); -#endif // ENABLE_RAYCAST_PICKING - shader->set_uniform("view_normal_matrix", view_normal_matrix); -#else - s_cube.set_color(-1, render_color); - s_cone.set_color(-1, render_color); - glsafe(::glPushMatrix()); - glsafe(::glTranslated(center.x(), center.y(), center.z())); - glsafe(::glRotated(Geometry::rad2deg(angles.z()), 0.0, 0.0, 1.0)); - glsafe(::glRotated(Geometry::rad2deg(angles.y()), 0.0, 1.0, 0.0)); - glsafe(::glRotated(Geometry::rad2deg(angles.x()), 1.0, 0.0, 0.0)); - glsafe(::glScaled(2.0 * half_size, 2.0 * half_size, 2.0 * half_size)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_RAYCAST_PICKING - s_cube.model.render(); - - auto render_extension = [&view_matrix, &view_matrix_no_offset, shader](const Transform3d& matrix) { - const Transform3d view_model_matrix = view_matrix * matrix; - shader->set_uniform("view_model_matrix", view_model_matrix); - const Matrix3d view_normal_matrix = view_matrix_no_offset * matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); - shader->set_uniform("view_normal_matrix", view_normal_matrix); - s_cone.model.render(); - }; -#else - s_cube.render(); -#endif // ENABLE_RAYCAST_PICKING - -#if ENABLE_LEGACY_OPENGL_REMOVAL - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[1] = elements_matrices[0] * Geometry::assemble_transform(Vec3d::UnitX(), Vec3d(0.0, 0.5 * double(PI), 0.0)); - render_extension(elements_matrices[1]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(Vec3d::UnitX(), Vec3d(0.0, 0.5 * double(PI), 0.0))); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[2] = elements_matrices[0] * Geometry::assemble_transform(-Vec3d::UnitX(), Vec3d(0.0, -0.5 * double(PI), 0.0)); - render_extension(elements_matrices[2]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(-Vec3d::UnitX(), Vec3d(0.0, -0.5 * double(PI), 0.0))); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[3] = elements_matrices[0] * Geometry::assemble_transform(Vec3d::UnitY(), Vec3d(-0.5 * double(PI), 0.0, 0.0)); - render_extension(elements_matrices[3]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(Vec3d::UnitY(), Vec3d(-0.5 * double(PI), 0.0, 0.0))); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[4] = elements_matrices[0] * Geometry::assemble_transform(-Vec3d::UnitY(), Vec3d(0.5 * double(PI), 0.0, 0.0)); - render_extension(elements_matrices[4]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(-Vec3d::UnitY(), Vec3d(0.5 * double(PI), 0.0, 0.0))); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[5] = elements_matrices[0] * Geometry::assemble_transform(Vec3d::UnitZ()); - render_extension(elements_matrices[5]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(Vec3d::UnitZ())); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[6] = elements_matrices[0] * Geometry::assemble_transform(-Vec3d::UnitZ(), Vec3d(double(PI), 0.0, 0.0)); - render_extension(elements_matrices[6]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(-Vec3d::UnitZ(), Vec3d(double(PI), 0.0, 0.0))); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } -#else - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(1.0, 0.0, 0.0)); - glsafe(::glRotated(0.5 * Geometry::rad2deg(double(PI)), 0.0, 1.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(-1.0, 0.0, 0.0)); - glsafe(::glRotated(-0.5 * Geometry::rad2deg(double(PI)), 0.0, 1.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(0.0, 1.0, 0.0)); - glsafe(::glRotated(-0.5 * Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(0.0, -1.0, 0.0)); - glsafe(::glRotated(0.5 * Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(0.0, 0.0, 1.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(0.0, 0.0, -1.0)); - glsafe(::glRotated(Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPopMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_RAYCAST_PICKING - if (raycasters[0] == nullptr) { - GLCanvas3D& canvas = *wxGetApp().plater()->canvas3D(); - raycasters[0] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cube.mesh_raycaster, elements_matrices[0]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) - raycasters[1] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[1]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) - raycasters[2] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[2]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) - raycasters[3] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[3]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) - raycasters[4] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[4]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) - raycasters[5] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[5]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) - raycasters[6] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[6]); - } - else { - for (size_t i = 0; i < GRABBER_ELEMENTS_MAX_COUNT; ++i) { - if (raycasters[i] != nullptr) - raycasters[i]->set_transform(elements_matrices[i]); - } - } -#endif // ENABLE_RAYCAST_PICKING -} - -GLGizmoBase::GLGizmoBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) - : m_parent(parent) - , m_icon_filename(icon_filename) - , m_sprite_id(sprite_id) - , m_imgui(wxGetApp().imgui()) -{ -} - -void GLGizmoBase::set_hover_id(int id) -{ - // do not change hover id during dragging - assert(!m_dragging); - - // allow empty grabbers when not using grabbers but use hover_id - flatten, rotate - if (!m_grabbers.empty() && id >= (int) m_grabbers.size()) - return; - - m_hover_id = id; - on_set_hover_id(); -} - -bool GLGizmoBase::update_items_state() -{ - bool res = m_dirty; - m_dirty = false; - return res; -} - -#if ENABLE_RAYCAST_PICKING -void GLGizmoBase::register_grabbers_for_picking() -{ - for (size_t i = 0; i < m_grabbers.size(); ++i) { - m_grabbers[i].register_raycasters_for_picking((m_group_id >= 0) ? m_group_id : i); - } -} - -void GLGizmoBase::unregister_grabbers_for_picking() -{ - for (size_t i = 0; i < m_grabbers.size(); ++i) { - m_grabbers[i].unregister_raycasters_for_picking(); - } -} -#else -ColorRGBA GLGizmoBase::picking_color_component(unsigned int id) const -{ - id = BASE_ID - id; - if (m_group_id > -1) - id -= m_group_id; - - return picking_decode(id); -} -#endif // ENABLE_RAYCAST_PICKING - -void GLGizmoBase::render_grabbers(const BoundingBoxf3& box) const -{ - render_grabbers((float)((box.size().x() + box.size().y() + box.size().z()) / 3.0)); -} - -void GLGizmoBase::render_grabbers(float size) const -{ - GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); - if (shader == nullptr) - return; - shader->start_using(); - shader->set_uniform("emission_factor", 0.1f); - for (int i = 0; i < (int)m_grabbers.size(); ++i) { - if (m_grabbers[i].enabled) - m_grabbers[i].render(m_hover_id == i, size); - } - shader->stop_using(); -} - -#if !ENABLE_RAYCAST_PICKING -void GLGizmoBase::render_grabbers_for_picking(const BoundingBoxf3& box) const -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader != nullptr) { - shader->start_using(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - const float mean_size = float((box.size().x() + box.size().y() + box.size().z()) / 3.0); - - for (unsigned int i = 0; i < (unsigned int)m_grabbers.size(); ++i) { - if (m_grabbers[i].enabled) { - m_grabbers[i].color = picking_color_component(i); - m_grabbers[i].render_for_picking(mean_size); - } - } -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} -#endif // !ENABLE_RAYCAST_PICKING - -// help function to process grabbers -// call start_dragging, stop_dragging, on_dragging -bool GLGizmoBase::use_grabbers(const wxMouseEvent &mouse_event) { - bool is_dragging_finished = false; - if (mouse_event.Moving()) { - // it should not happen but for sure - assert(!m_dragging); - if (m_dragging) is_dragging_finished = true; - else return false; - } - - if (mouse_event.LeftDown()) { - Selection &selection = m_parent.get_selection(); - if (!selection.is_empty() && m_hover_id != -1 && - (m_grabbers.empty() || m_hover_id < static_cast(m_grabbers.size()))) { - selection.setup_cache(); - - m_dragging = true; - for (auto &grabber : m_grabbers) grabber.dragging = false; - if (!m_grabbers.empty() && m_hover_id < int(m_grabbers.size())) - m_grabbers[m_hover_id].dragging = true; - - on_start_dragging(); - - // Let the plater know that the dragging started - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED)); - m_parent.set_as_dirty(); - return true; - } - } else if (m_dragging) { - // when mouse cursor leave window than finish actual dragging operation - bool is_leaving = mouse_event.Leaving(); - if (mouse_event.Dragging()) { - Point mouse_coord(mouse_event.GetX(), mouse_event.GetY()); - auto ray = m_parent.mouse_ray(mouse_coord); - UpdateData data(ray, mouse_coord); - - on_dragging(data); - - wxGetApp().obj_manipul()->set_dirty(); - m_parent.set_as_dirty(); - return true; - } - else if (mouse_event.LeftUp() || is_leaving || is_dragging_finished) { -#if ENABLE_WORLD_COORDINATE - do_stop_dragging(is_leaving); -#else - for (auto &grabber : m_grabbers) grabber.dragging = false; - m_dragging = false; - - // NOTE: This should be part of GLCanvas3D - // Reset hover_id when leave window - if (is_leaving) m_parent.mouse_up_cleanup(); - - on_stop_dragging(); - - // There is prediction that after draggign, data are changed - // Data are updated twice also by canvas3D::reload_scene. - // Should be fixed. - m_parent.get_gizmos_manager().update_data(); - - wxGetApp().obj_manipul()->set_dirty(); - - // Let the plater know that the dragging finished, so a delayed - // refresh of the scene with the background processing data should - // be performed. - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); - // updates camera target constraints - m_parent.refresh_camera_scene_box(); -#endif // ENABLE_WORLD_COORDINATE - return true; - } - } - return false; -} - -#if ENABLE_WORLD_COORDINATE -void GLGizmoBase::do_stop_dragging(bool perform_mouse_cleanup) -{ - for (auto& grabber : m_grabbers) grabber.dragging = false; - m_dragging = false; - - // NOTE: This should be part of GLCanvas3D - // Reset hover_id when leave window - if (perform_mouse_cleanup) m_parent.mouse_up_cleanup(); - - on_stop_dragging(); - - // There is prediction that after draggign, data are changed - // Data are updated twice also by canvas3D::reload_scene. - // Should be fixed. - m_parent.get_gizmos_manager().update_data(); - - wxGetApp().obj_manipul()->set_dirty(); - - // Let the plater know that the dragging finished, so a delayed - // refresh of the scene with the background processing data should - // be performed. - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); - // updates camera target constraints - m_parent.refresh_camera_scene_box(); -} -#endif // ENABLE_WORLD_COORDINATE - -std::string GLGizmoBase::format(float value, unsigned int decimals) const -{ - return Slic3r::string_printf("%.*f", decimals, value); -} - -void GLGizmoBase::set_dirty() { - m_dirty = true; -} - - - -void GLGizmoBase::render_input_window(float x, float y, float bottom_limit) -{ - on_render_input_window(x, y, bottom_limit); - if (m_first_input_window_render) { - // imgui windows that don't have an initial size needs to be processed once to get one - // and are not rendered in the first frame - // so, we forces to render another frame the first time the imgui window is shown - // https://github.com/ocornut/imgui/issues/2949 - m_parent.set_as_dirty(); - m_parent.request_extra_frame(); - m_first_input_window_render = false; - } -} - - - -std::string GLGizmoBase::get_name(bool include_shortcut) const -{ - int key = get_shortcut_key(); - std::string out = on_get_name(); - if (include_shortcut && key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z) - out += std::string(" [") + char(int('A') + key - int(WXK_CONTROL_A)) + "]"; - return out; -} - - -} // namespace GUI -} // namespace Slic3r - +#include "GLGizmoBase.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" + +#include + +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#if ENABLE_LEGACY_OPENGL_REMOVAL +#include "slic3r/GUI/Plater.hpp" +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +// TODO: Display tooltips quicker on Linux + +namespace Slic3r { +namespace GUI { + +const float GLGizmoBase::Grabber::SizeFactor = 0.05f; +const float GLGizmoBase::Grabber::MinHalfSize = 1.5f; +const float GLGizmoBase::Grabber::DraggingScaleFactor = 1.25f; + +#if ENABLE_RAYCAST_PICKING +PickingModel GLGizmoBase::Grabber::s_cube; +PickingModel GLGizmoBase::Grabber::s_cone; +#else +GLModel GLGizmoBase::Grabber::s_cube; +GLModel GLGizmoBase::Grabber::s_cone; +#endif // ENABLE_RAYCAST_PICKING + +GLGizmoBase::Grabber::~Grabber() +{ +#if ENABLE_RAYCAST_PICKING + if (s_cube.model.is_initialized()) + s_cube.model.reset(); + + if (s_cone.model.is_initialized()) + s_cone.model.reset(); +#else + if (s_cube.is_initialized()) + s_cube.reset(); + + if (s_cone.is_initialized()) + s_cone.reset(); +#endif // ENABLE_RAYCAST_PICKING +} + +float GLGizmoBase::Grabber::get_half_size(float size) const +{ + return std::max(size * SizeFactor, MinHalfSize); +} + +float GLGizmoBase::Grabber::get_dragging_half_size(float size) const +{ + return get_half_size(size) * DraggingScaleFactor; +} + +#if ENABLE_RAYCAST_PICKING +void GLGizmoBase::Grabber::register_raycasters_for_picking(int id) +{ + picking_id = id; + // registration will happen on next call to render() +} + +void GLGizmoBase::Grabber::unregister_raycasters_for_picking() +{ + wxGetApp().plater()->canvas3D()->remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, picking_id); + picking_id = -1; + raycasters = { nullptr }; +} +#endif // ENABLE_RAYCAST_PICKING + +#if ENABLE_RAYCAST_PICKING +void GLGizmoBase::Grabber::render(float size, const ColorRGBA& render_color) +#else +void GLGizmoBase::Grabber::render(float size, const ColorRGBA& render_color, bool picking) +#endif // ENABLE_RAYCAST_PICKING +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_current_shader(); + if (shader == nullptr) + return; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_RAYCAST_PICKING + if (!s_cube.model.is_initialized()) { +#else + if (!s_cube.is_initialized()) { +#endif // ENABLE_RAYCAST_PICKING + // This cannot be done in constructor, OpenGL is not yet + // initialized at that point (on Linux at least). + indexed_triangle_set its = its_make_cube(1.0, 1.0, 1.0); + its_translate(its, -0.5f * Vec3f::Ones()); +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_RAYCAST_PICKING + s_cube.model.init_from(its); + s_cube.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); +#else + s_cube.init_from(its); +#endif // ENABLE_RAYCAST_PICKING +#else + s_cube.init_from(its, BoundingBoxf3{ { -0.5, -0.5, -0.5 }, { 0.5, 0.5, 0.5 } }); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + +#if ENABLE_RAYCAST_PICKING + if (!s_cone.model.is_initialized()) { + indexed_triangle_set its = its_make_cone(0.375, 1.5, double(PI) / 18.0); + s_cone.model.init_from(its); + s_cone.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); + } +#else + if (!s_cone.is_initialized()) + s_cone.init_from(its_make_cone(0.375, 1.5, double(PI) / 18.0)); +#endif // ENABLE_RAYCAST_PICKING + + const float half_size = dragging ? get_dragging_half_size(size) : get_half_size(size); + +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_RAYCAST_PICKING + s_cube.model.set_color(render_color); + s_cone.model.set_color(render_color); +#else + s_cube.set_color(render_color); + s_cone.set_color(render_color); +#endif // ENABLE_RAYCAST_PICKING + + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#if ENABLE_RAYCAST_PICKING + const Transform3d& view_matrix = camera.get_view_matrix(); + const Matrix3d view_matrix_no_offset = view_matrix.matrix().block(0, 0, 3, 3); + std::vector elements_matrices(GRABBER_ELEMENTS_MAX_COUNT, Transform3d::Identity()); + elements_matrices[0] = matrix * Geometry::assemble_transform(center, angles, 2.0 * half_size * Vec3d::Ones()); + Transform3d view_model_matrix = view_matrix * elements_matrices[0]; +#else + const Transform3d& view_matrix = camera.get_view_matrix(); + const Transform3d model_matrix = matrix * Geometry::assemble_transform(center, angles, 2.0 * half_size * Vec3d::Ones()); + const Transform3d view_model_matrix = view_matrix * model_matrix; +#endif // ENABLE_RAYCAST_PICKING + + shader->set_uniform("view_model_matrix", view_model_matrix); +#if ENABLE_RAYCAST_PICKING + Matrix3d view_normal_matrix = view_matrix_no_offset * elements_matrices[0].matrix().block(0, 0, 3, 3).inverse().transpose(); +#else + const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); +#endif // ENABLE_RAYCAST_PICKING + shader->set_uniform("view_normal_matrix", view_normal_matrix); +#else + s_cube.set_color(-1, render_color); + s_cone.set_color(-1, render_color); + glsafe(::glPushMatrix()); + glsafe(::glTranslated(center.x(), center.y(), center.z())); + glsafe(::glRotated(Geometry::rad2deg(angles.z()), 0.0, 0.0, 1.0)); + glsafe(::glRotated(Geometry::rad2deg(angles.y()), 0.0, 1.0, 0.0)); + glsafe(::glRotated(Geometry::rad2deg(angles.x()), 1.0, 0.0, 0.0)); + glsafe(::glScaled(2.0 * half_size, 2.0 * half_size, 2.0 * half_size)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_RAYCAST_PICKING + s_cube.model.render(); + + auto render_extension = [&view_matrix, &view_matrix_no_offset, shader](const Transform3d& matrix) { + const Transform3d view_model_matrix = view_matrix * matrix; + shader->set_uniform("view_model_matrix", view_model_matrix); + const Matrix3d view_normal_matrix = view_matrix_no_offset * matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader->set_uniform("view_normal_matrix", view_normal_matrix); + s_cone.model.render(); + }; +#else + s_cube.render(); +#endif // ENABLE_RAYCAST_PICKING + +#if ENABLE_LEGACY_OPENGL_REMOVAL + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[1] = elements_matrices[0] * Geometry::assemble_transform(Vec3d::UnitX(), Vec3d(0.0, 0.5 * double(PI), 0.0)); + render_extension(elements_matrices[1]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(Vec3d::UnitX(), Vec3d(0.0, 0.5 * double(PI), 0.0))); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[2] = elements_matrices[0] * Geometry::assemble_transform(-Vec3d::UnitX(), Vec3d(0.0, -0.5 * double(PI), 0.0)); + render_extension(elements_matrices[2]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(-Vec3d::UnitX(), Vec3d(0.0, -0.5 * double(PI), 0.0))); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[3] = elements_matrices[0] * Geometry::assemble_transform(Vec3d::UnitY(), Vec3d(-0.5 * double(PI), 0.0, 0.0)); + render_extension(elements_matrices[3]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(Vec3d::UnitY(), Vec3d(-0.5 * double(PI), 0.0, 0.0))); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[4] = elements_matrices[0] * Geometry::assemble_transform(-Vec3d::UnitY(), Vec3d(0.5 * double(PI), 0.0, 0.0)); + render_extension(elements_matrices[4]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(-Vec3d::UnitY(), Vec3d(0.5 * double(PI), 0.0, 0.0))); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[5] = elements_matrices[0] * Geometry::assemble_transform(Vec3d::UnitZ()); + render_extension(elements_matrices[5]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(Vec3d::UnitZ())); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[6] = elements_matrices[0] * Geometry::assemble_transform(-Vec3d::UnitZ(), Vec3d(double(PI), 0.0, 0.0)); + render_extension(elements_matrices[6]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(-Vec3d::UnitZ(), Vec3d(double(PI), 0.0, 0.0))); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } +#else + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(1.0, 0.0, 0.0)); + glsafe(::glRotated(0.5 * Geometry::rad2deg(double(PI)), 0.0, 1.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(-1.0, 0.0, 0.0)); + glsafe(::glRotated(-0.5 * Geometry::rad2deg(double(PI)), 0.0, 1.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0.0, 1.0, 0.0)); + glsafe(::glRotated(-0.5 * Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0.0, -1.0, 0.0)); + glsafe(::glRotated(0.5 * Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0.0, 0.0, 1.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0.0, 0.0, -1.0)); + glsafe(::glRotated(Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_RAYCAST_PICKING + if (raycasters[0] == nullptr) { + GLCanvas3D& canvas = *wxGetApp().plater()->canvas3D(); + raycasters[0] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cube.mesh_raycaster, elements_matrices[0]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) + raycasters[1] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[1]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) + raycasters[2] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[2]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) + raycasters[3] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[3]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) + raycasters[4] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[4]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) + raycasters[5] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[5]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) + raycasters[6] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[6]); + } + else { + for (size_t i = 0; i < GRABBER_ELEMENTS_MAX_COUNT; ++i) { + if (raycasters[i] != nullptr) + raycasters[i]->set_transform(elements_matrices[i]); + } + } +#endif // ENABLE_RAYCAST_PICKING +} + +GLGizmoBase::GLGizmoBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : m_parent(parent) + , m_icon_filename(icon_filename) + , m_sprite_id(sprite_id) + , m_imgui(wxGetApp().imgui()) +{ +} + +void GLGizmoBase::set_hover_id(int id) +{ + // do not change hover id during dragging + assert(!m_dragging); + + // allow empty grabbers when not using grabbers but use hover_id - flatten, rotate +// if (!m_grabbers.empty() && id >= (int) m_grabbers.size()) +// return; + + m_hover_id = id; + on_set_hover_id(); +} + +bool GLGizmoBase::update_items_state() +{ + bool res = m_dirty; + m_dirty = false; + return res; +} + +#if ENABLE_RAYCAST_PICKING +void GLGizmoBase::register_grabbers_for_picking() +{ + for (size_t i = 0; i < m_grabbers.size(); ++i) { + m_grabbers[i].register_raycasters_for_picking((m_group_id >= 0) ? m_group_id : i); + } +} + +void GLGizmoBase::unregister_grabbers_for_picking() +{ + for (size_t i = 0; i < m_grabbers.size(); ++i) { + m_grabbers[i].unregister_raycasters_for_picking(); + } +} +#else +ColorRGBA GLGizmoBase::picking_color_component(unsigned int id) const +{ + id = BASE_ID - id; + if (m_group_id > -1) + id -= m_group_id; + + return picking_decode(id); +} +#endif // ENABLE_RAYCAST_PICKING + +void GLGizmoBase::render_grabbers(const BoundingBoxf3& box) const +{ + render_grabbers((float)((box.size().x() + box.size().y() + box.size().z()) / 3.0)); +} + +void GLGizmoBase::render_grabbers(float size) const +{ + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + shader->start_using(); + shader->set_uniform("emission_factor", 0.1f); + for (int i = 0; i < (int)m_grabbers.size(); ++i) { + if (m_grabbers[i].enabled) + m_grabbers[i].render(m_hover_id == i, size); + } + shader->stop_using(); +} + +#if !ENABLE_RAYCAST_PICKING +void GLGizmoBase::render_grabbers_for_picking(const BoundingBoxf3& box) const +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader != nullptr) { + shader->start_using(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + const float mean_size = float((box.size().x() + box.size().y() + box.size().z()) / 3.0); + + for (unsigned int i = 0; i < (unsigned int)m_grabbers.size(); ++i) { + if (m_grabbers[i].enabled) { + m_grabbers[i].color = picking_color_component(i); + m_grabbers[i].render_for_picking(mean_size); + } + } +#if ENABLE_LEGACY_OPENGL_REMOVAL + shader->stop_using(); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} +#endif // !ENABLE_RAYCAST_PICKING + +// help function to process grabbers +// call start_dragging, stop_dragging, on_dragging +bool GLGizmoBase::use_grabbers(const wxMouseEvent &mouse_event) { + bool is_dragging_finished = false; + if (mouse_event.Moving()) { + // it should not happen but for sure + assert(!m_dragging); + if (m_dragging) is_dragging_finished = true; + else return false; + } + + if (mouse_event.LeftDown()) { + Selection &selection = m_parent.get_selection(); + if (!selection.is_empty() && m_hover_id != -1 /* && + (m_grabbers.empty() || m_hover_id < static_cast(m_grabbers.size()))*/) { + selection.setup_cache(); + + m_dragging = true; + for (auto &grabber : m_grabbers) grabber.dragging = false; +// if (!m_grabbers.empty() && m_hover_id < int(m_grabbers.size())) +// m_grabbers[m_hover_id].dragging = true; + + on_start_dragging(); + + // Let the plater know that the dragging started + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED)); + m_parent.set_as_dirty(); + return true; + } + } else if (m_dragging) { + // when mouse cursor leave window than finish actual dragging operation + bool is_leaving = mouse_event.Leaving(); + if (mouse_event.Dragging()) { + Point mouse_coord(mouse_event.GetX(), mouse_event.GetY()); + auto ray = m_parent.mouse_ray(mouse_coord); + UpdateData data(ray, mouse_coord); + + on_dragging(data); + + wxGetApp().obj_manipul()->set_dirty(); + m_parent.set_as_dirty(); + return true; + } + else if (mouse_event.LeftUp() || is_leaving || is_dragging_finished) { +#if ENABLE_WORLD_COORDINATE + do_stop_dragging(is_leaving); +#else + for (auto &grabber : m_grabbers) grabber.dragging = false; + m_dragging = false; + + // NOTE: This should be part of GLCanvas3D + // Reset hover_id when leave window + if (is_leaving) m_parent.mouse_up_cleanup(); + + on_stop_dragging(); + + // There is prediction that after draggign, data are changed + // Data are updated twice also by canvas3D::reload_scene. + // Should be fixed. + m_parent.get_gizmos_manager().update_data(); + + wxGetApp().obj_manipul()->set_dirty(); + + // Let the plater know that the dragging finished, so a delayed + // refresh of the scene with the background processing data should + // be performed. + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); + // updates camera target constraints + m_parent.refresh_camera_scene_box(); +#endif // ENABLE_WORLD_COORDINATE + return true; + } + } + return false; +} + +#if ENABLE_WORLD_COORDINATE +void GLGizmoBase::do_stop_dragging(bool perform_mouse_cleanup) +{ + for (auto& grabber : m_grabbers) grabber.dragging = false; + m_dragging = false; + + // NOTE: This should be part of GLCanvas3D + // Reset hover_id when leave window + if (perform_mouse_cleanup) m_parent.mouse_up_cleanup(); + + on_stop_dragging(); + + // There is prediction that after draggign, data are changed + // Data are updated twice also by canvas3D::reload_scene. + // Should be fixed. + m_parent.get_gizmos_manager().update_data(); + + wxGetApp().obj_manipul()->set_dirty(); + + // Let the plater know that the dragging finished, so a delayed + // refresh of the scene with the background processing data should + // be performed. + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); + // updates camera target constraints + m_parent.refresh_camera_scene_box(); +} +#endif // ENABLE_WORLD_COORDINATE + +std::string GLGizmoBase::format(float value, unsigned int decimals) const +{ + return Slic3r::string_printf("%.*f", decimals, value); +} + +void GLGizmoBase::set_dirty() { + m_dirty = true; +} + + + +void GLGizmoBase::render_input_window(float x, float y, float bottom_limit) +{ + on_render_input_window(x, y, bottom_limit); + if (m_first_input_window_render) { + // imgui windows that don't have an initial size needs to be processed once to get one + // and are not rendered in the first frame + // so, we forces to render another frame the first time the imgui window is shown + // https://github.com/ocornut/imgui/issues/2949 + m_parent.set_as_dirty(); + m_parent.request_extra_frame(); + m_first_input_window_render = false; + } +} + + + +std::string GLGizmoBase::get_name(bool include_shortcut) const +{ + int key = get_shortcut_key(); + std::string out = on_get_name(); + if (include_shortcut && key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z) + out += std::string(" [") + char(int('A') + key - int(WXK_CONTROL_A)) + "]"; + return out; +} + + +} // namespace GUI +} // namespace Slic3r + diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp index 09766bbac..43624964a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp @@ -210,7 +210,10 @@ public: #if ENABLE_RAYCAST_PICKING void register_raycasters_for_picking() { register_grabbers_for_picking(); on_register_raycasters_for_picking(); } void unregister_raycasters_for_picking() { unregister_grabbers_for_picking(); on_unregister_raycasters_for_picking(); } -#endif // ENABLE_RAYCAST_PICKING +#endif // ENABLE_RAYCAST_PICKING + + virtual bool is_in_editing_mode() const { return false; } + virtual bool is_selection_rectangle_dragging() const { return false; } protected: virtual bool on_init() = 0; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 8767fecb2..14b31d7c5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -4,465 +4,2276 @@ #include -#include -#include -#include -#include - #include #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/Utils/UndoRedo.hpp" #include "libslic3r/AppConfig.hpp" -#include "libslic3r/Model.hpp" #include "libslic3r/TriangleMeshSlicer.hpp" +#include "imgui/imgui_internal.h" + namespace Slic3r { namespace GUI { -const double GLGizmoCut::Offset = 10.0; -const double GLGizmoCut::Margin = 20.0; -static const ColorRGBA GRABBER_COLOR = ColorRGBA::ORANGE(); +static const ColorRGBA GRABBER_COLOR = ColorRGBA::YELLOW(); -GLGizmoCut::GLGizmoCut(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) +// connector colors +static const ColorRGBA PLAG_COLOR = ColorRGBA::YELLOW(); +static const ColorRGBA DOWEL_COLOR = ColorRGBA::DARK_YELLOW(); +static const ColorRGBA HOVERED_PLAG_COLOR = ColorRGBA::CYAN(); +static const ColorRGBA HOVERED_DOWEL_COLOR = ColorRGBA(0.0f, 0.5f, 0.5f, 1.0f); +static const ColorRGBA SELECTED_PLAG_COLOR = ColorRGBA::GRAY(); +static const ColorRGBA SELECTED_DOWEL_COLOR = ColorRGBA::DARK_GRAY(); +static const ColorRGBA CONNECTOR_DEF_COLOR = ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f); +static const ColorRGBA CONNECTOR_ERR_COLOR = ColorRGBA(1.0f, 0.3f, 0.3f, 0.5f); + +const unsigned int AngleResolution = 64; +const unsigned int ScaleStepsCount = 72; +const float ScaleStepRad = 2.0f * float(PI) / ScaleStepsCount; +const unsigned int ScaleLongEvery = 2; +const float ScaleLongTooth = 0.1f; // in percent of radius +const unsigned int SnapRegionsCount = 8; + +const float UndefFloat = -999.f; +const std::string UndefLabel = " "; + +using namespace Geometry; + +// Generates mesh for a line +static GLModel::Geometry its_make_line(Vec3f beg_pos, Vec3f end_pos) +{ + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(2); + init_data.reserve_indices(2); + + // vertices + init_data.add_vertex(beg_pos); + init_data.add_vertex(end_pos); + + // indices + init_data.add_line(0, 1); + return init_data; +} + +// Generates mesh for a square plane +static GLModel::Geometry its_make_square_plane(float radius) +{ + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(6); + + // vertices + init_data.add_vertex(Vec3f(-radius, -radius, 0.0)); + init_data.add_vertex(Vec3f(radius , -radius, 0.0)); + init_data.add_vertex(Vec3f(radius , radius , 0.0)); + init_data.add_vertex(Vec3f(-radius, radius , 0.0)); + + // indices + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(2, 3, 0); + return init_data; +} + +//! -- #ysFIXME those functions bodies are ported from GizmoRotation +// Generates mesh for a circle +static void init_from_circle(GLModel& model, double radius) +{ + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::LineLoop, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(ScaleStepsCount); + init_data.reserve_indices(ScaleStepsCount); + + // vertices + indices + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i * ScaleStepRad); + init_data.add_vertex(Vec3f(::cos(angle) * float(radius), ::sin(angle) * float(radius), 0.0f)); + init_data.add_index(i); + } + + model.init_from(std::move(init_data)); + model.set_color(ColorRGBA::WHITE()); +} + +// Generates mesh for a scale +static void init_from_scale(GLModel& model, double radius) +{ + const float out_radius_long = float(radius) * (1.0f + ScaleLongTooth); + const float out_radius_short = float(radius) * (1.0f + 0.5f * ScaleLongTooth); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(2 * ScaleStepsCount); + init_data.reserve_indices(2 * ScaleStepsCount); + + // vertices + indices + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i * ScaleStepRad); + const float cosa = ::cos(angle); + const float sina = ::sin(angle); + const float in_x = cosa * float(radius); + const float in_y = sina * float(radius); + const float out_x = (i % ScaleLongEvery == 0) ? cosa * out_radius_long : cosa * out_radius_short; + const float out_y = (i % ScaleLongEvery == 0) ? sina * out_radius_long : sina * out_radius_short; + + // vertices + init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); + init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); + + // indices + init_data.add_line(i * 2, i * 2 + 1); + } + + model.init_from(std::move(init_data)); + model.set_color(ColorRGBA::WHITE()); +} + +// Generates mesh for a snap_radii +static void init_from_snap_radii(GLModel& model, double radius) +{ + const float step = 2.0f * float(PI) / float(SnapRegionsCount); + const float in_radius = float(radius) / 3.0f; + const float out_radius = 2.0f * in_radius; + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(2 * ScaleStepsCount); + init_data.reserve_indices(2 * ScaleStepsCount); + + // vertices + indices + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i) * step; + const float cosa = ::cos(angle); + const float sina = ::sin(angle); + const float in_x = cosa * in_radius; + const float in_y = sina * in_radius; + const float out_x = cosa * out_radius; + const float out_y = sina * out_radius; + + // vertices + init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); + init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); + + // indices + init_data.add_line(i * 2, i * 2 + 1); + } + + model.init_from(std::move(init_data)); + model.set_color(ColorRGBA::WHITE()); +} + +// Generates mesh for a angle_arc +static void init_from_angle_arc(GLModel& model, double angle, double radius) +{ + model.reset(); + + const float step_angle = float(angle) / float(AngleResolution); + const float ex_radius = float(radius); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::LineStrip, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(1 + AngleResolution); + init_data.reserve_indices(1 + AngleResolution); + + // vertices + indices + for (unsigned int i = 0; i <= AngleResolution; ++i) { + const float angle = float(i) * step_angle; + init_data.add_vertex(Vec3f(::cos(angle) * ex_radius, ::sin(angle) * ex_radius, 0.0f)); + init_data.add_index(i); + } + + model.init_from(std::move(init_data)); +} + +//! -- + +GLGizmoCut3D::GLGizmoCut3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) : GLGizmoBase(parent, icon_filename, sprite_id) -{} - -std::string GLGizmoCut::get_tooltip() const + , m_connectors_group_id (3) + , m_connector_type (CutConnectorType::Plug) + , m_connector_style (size_t(CutConnectorStyle::Prizm)) + , m_connector_shape_id (size_t(CutConnectorShape::Circle)) { - double cut_z = m_cut_z; - if (wxGetApp().app_config->get("use_inches") == "1") - cut_z *= ObjectManipulation::mm_to_in; + m_modes = { _u8L("Planar")//, _u8L("Grid") +// , _u8L("Radial"), _u8L("Modular") + }; - return (m_hover_id == 0 || m_grabbers[0].dragging) ? "Z: " + format(cut_z, 2) : ""; + m_connector_modes = { _u8L("Auto"), _u8L("Manual") }; + m_connector_types = { _u8L("Plug"), _u8L("Dowel") }; + + m_connector_styles = { _u8L("Prizm"), _u8L("Frustum") +// , _u8L("Claw") + }; + + m_connector_shapes = { _u8L("Triangle"), _u8L("Square"), _u8L("Hexagon"), _u8L("Circle") +// , _u8L("D-shape") + }; + + m_axis_names = { "X", "Y", "Z" }; + + update_connector_shape(); } -bool GLGizmoCut::on_mouse(const wxMouseEvent &mouse_event) +std::string GLGizmoCut3D::get_tooltip() const { - return use_grabbers(mouse_event); + std::string tooltip; + if (m_hover_id == Z) { + double koef = m_imperial_units ? ObjectManipulation::mm_to_in : 1.0; + std::string unit_str = " " + (m_imperial_units ? _u8L("inch") : _u8L("mm")); + const BoundingBoxf3 tbb = transformed_bounding_box(m_plane_center); + if (tbb.max.z() >= 0.0) { + double top = (tbb.min.z() <= 0.0 ? tbb.max.z() : tbb.size().z()) * koef; + tooltip += format(top, 2) + " " + unit_str + " (" + _u8L("Top part") + ")"; + if (tbb.min.z() <= 0.0) + tooltip += "\n"; + } + if (tbb.min.z() <= 0.0) { + double bottom = (tbb.max.z() <= 0.0 ? tbb.size().z() : (tbb.min.z() * (-1))) * koef; + tooltip += format(bottom, 2) + " " + unit_str + " (" + _u8L("Bottom part") + ")"; + } + return tooltip; + } + if (tooltip.empty() && (m_hover_id == X || m_hover_id == Y)) { + std::string axis = m_hover_id == X ? "X" : "Y"; + return axis + ": " + format(float(rad2deg(m_angle)), 1) + _u8L("°"); + } + + return tooltip; } -bool GLGizmoCut::on_init() +bool GLGizmoCut3D::on_mouse(const wxMouseEvent &mouse_event) { - m_grabbers.emplace_back(); - m_shortcut_key = WXK_CONTROL_C; - return true; + Vec2i mouse_coord(mouse_event.GetX(), mouse_event.GetY()); + Vec2d mouse_pos = mouse_coord.cast(); + + if (mouse_event.ShiftDown() && mouse_event.LeftDown()) + return gizmo_event(SLAGizmoEventType::LeftDown, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); + if (cut_line_processing()) { + if (mouse_event.ShiftDown()) { + if (mouse_event.Moving()|| mouse_event.Dragging()) + return gizmo_event(SLAGizmoEventType::Moving, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); + if (mouse_event.LeftUp()) + return gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); + } + discard_cut_line_processing(); + } + else if (mouse_event.Moving()) + return false; + + if (use_grabbers(mouse_event)) { + if (m_hover_id >= m_connectors_group_id) { + if (mouse_event.LeftDown() && !mouse_event.CmdDown()&& !mouse_event.AltDown()) + unselect_all_connectors(); + if (mouse_event.LeftUp() && !mouse_event.ShiftDown()) + gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); + } + return true; + } + + static bool pending_right_up = false; + if (mouse_event.LeftDown()) { + bool grabber_contains_mouse = (get_hover_id() != -1); + const bool shift_down = mouse_event.ShiftDown(); + if ((!shift_down || grabber_contains_mouse) && + gizmo_event(SLAGizmoEventType::LeftDown, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false)) + return true; + } + else if (mouse_event.Dragging()) { + bool control_down = mouse_event.CmdDown(); + if (m_parent.get_move_volume_id() != -1) { + // don't allow dragging objects with the Sla gizmo on + return true; + } + if (!control_down && + gizmo_event(SLAGizmoEventType::Dragging, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false)) { + // the gizmo got the event and took some action, no need to do + // anything more here + m_parent.set_as_dirty(); + return true; + } + if (control_down && (mouse_event.LeftIsDown() || mouse_event.RightIsDown())) { + // CTRL has been pressed while already dragging -> stop current action + if (mouse_event.LeftIsDown()) + gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), true); + else if (mouse_event.RightIsDown()) + pending_right_up = false; + } + } + else if (mouse_event.LeftUp() && !m_parent.is_mouse_dragging()) { + // in case SLA/FDM gizmo is selected, we just pass the LeftUp event + // and stop processing - neither object moving or selecting is + // suppressed in that case + gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); + return true; + } + else if (mouse_event.RightDown()) { + if (m_parent.get_selection().get_object_idx() != -1 && + gizmo_event(SLAGizmoEventType::RightDown, mouse_pos, false, false, false)) { + // we need to set the following right up as processed to avoid showing + // the context menu if the user release the mouse over the object + pending_right_up = true; + // event was taken care of by the SlaSupports gizmo + return true; + } + } + else if (pending_right_up && mouse_event.RightUp()) { + pending_right_up = false; + return true; + } + return false; } -std::string GLGizmoCut::on_get_name() const +void GLGizmoCut3D::shift_cut_z(double delta) { - return _u8L("Cut"); + Vec3d new_cut_center = m_plane_center; + new_cut_center[Z] += delta; + set_center(new_cut_center); } -void GLGizmoCut::on_set_state() +void GLGizmoCut3D::rotate_vec3d_around_plane_center(Vec3d&vec) { - // Reset m_cut_z on gizmo activation - if (m_state == On) - m_cut_z = bounding_box().center().z(); + vec = Transformation( assemble_transform(m_plane_center) * m_rotation_m * assemble_transform(-m_plane_center)).get_matrix() * vec; } -bool GLGizmoCut::on_is_activable() const +void GLGizmoCut3D::put_connectors_on_cut_plane(const Vec3d& cp_normal, double cp_offset) { - const Selection& selection = m_parent.get_selection(); - return selection.is_single_full_instance() && !selection.is_wipe_tower(); + ModelObject* mo = m_c->selection_info()->model_object(); + if (CutConnectors& connectors = mo->cut_connectors; !connectors.empty()) { + const float sla_shift = m_c->selection_info()->get_sla_shift(); + const Vec3d& instance_offset = mo->instances[m_c->selection_info()->get_active_instance()]->get_offset(); + + for (auto& connector : connectors) { + // convert connetor pos to the world coordinates + Vec3d pos = connector.pos + instance_offset; + pos[Z] += sla_shift; + // scalar distance from point to plane along the normal + double distance = -cp_normal.dot(pos) + cp_offset; + // move connector + connector.pos += distance * cp_normal; + } + } } -void GLGizmoCut::on_start_dragging() +void GLGizmoCut3D::update_clipper() { - if (m_hover_id == -1) + BoundingBoxf3 box = bounding_box(); + + Vec3d beg, end = beg = m_plane_center; + beg[Z] = box.center().z() - m_radius; + end[Z] = box.center().z() + m_radius; + + rotate_vec3d_around_plane_center(beg); + rotate_vec3d_around_plane_center(end); + + double dist = (m_plane_center - beg).norm(); + + // calculate normal and offset for clipping plane + Vec3d normal = end - beg; + if (normal == Vec3d::Zero()) + return; + dist = std::clamp(dist, 0.0001, normal.norm()); + normal.normalize(); + const double offset = normal.dot(beg) + dist; + + m_c->object_clipper()->set_range_and_pos(normal, offset, dist); + + put_connectors_on_cut_plane(normal, offset); + + if (m_raycasters.empty()) + on_register_raycasters_for_picking(); + else + update_raycasters_for_picking_transform(); +} + +void GLGizmoCut3D::update_clipper_on_render() +{ + update_clipper(); + force_update_clipper_on_render = false; +} + +void GLGizmoCut3D::set_center(const Vec3d& center) +{ + set_center_pos(center); + update_clipper(); +} + +bool GLGizmoCut3D::render_combo(const std::string& label, const std::vector& lines, size_t& selection_idx) +{ + ImGui::AlignTextToFramePadding(); + m_imgui->text(label); + ImGui::SameLine(m_label_width); + ImGui::PushItemWidth(m_control_width); + + size_t selection_out = selection_idx; + // It is necessary to use BeginGroup(). Otherwise, when using SameLine() is called, then other items will be drawn inside the combobox. + ImGui::BeginGroup(); + ImVec2 combo_pos = ImGui::GetCursorScreenPos(); + if (ImGui::BeginCombo(("##"+label).c_str(), "")) { + for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { + ImGui::PushID(int(line_idx)); + if (ImGui::Selectable("", line_idx == selection_idx)) + selection_out = line_idx; + + ImGui::SameLine(); + ImGui::Text("%s", lines[line_idx].c_str()); + ImGui::PopID(); + } + + ImGui::EndCombo(); + } + + ImVec2 backup_pos = ImGui::GetCursorScreenPos(); + ImGuiStyle& style = ImGui::GetStyle(); + + ImGui::SetCursorScreenPos(ImVec2(combo_pos.x + style.FramePadding.x, combo_pos.y + style.FramePadding.y)); + ImGui::Text("%s", selection_out < lines.size() ? lines[selection_out].c_str() : UndefLabel.c_str()); + ImGui::SetCursorScreenPos(backup_pos); + ImGui::EndGroup(); + + bool is_changed = selection_idx != selection_out; + selection_idx = selection_out; + + if (is_changed) + update_connector_shape(); + + return is_changed; +} + +bool GLGizmoCut3D::render_double_input(const std::string& label, double& value_in) +{ + ImGui::AlignTextToFramePadding(); + m_imgui->text(label); + ImGui::SameLine(m_label_width); + ImGui::PushItemWidth(m_control_width); + + double value = value_in; + if (m_imperial_units) + value *= ObjectManipulation::mm_to_in; + double old_val = value; + ImGui::InputDouble(("##" + label).c_str(), &value, 0.0f, 0.0f, "%.2f", ImGuiInputTextFlags_CharsDecimal); + + ImGui::SameLine(); + m_imgui->text(m_imperial_units ? _L("in") : _L("mm")); + + value_in = value * (m_imperial_units ? ObjectManipulation::in_to_mm : 1.0); + return !is_approx(old_val, value); +} + +bool GLGizmoCut3D::render_slider_double_input(const std::string& label, float& value_in, float& tolerance_in) +{ + ImGui::AlignTextToFramePadding(); + m_imgui->text(label); + ImGui::SameLine(m_label_width); + ImGui::PushItemWidth(m_control_width * 0.85f); + + float value = value_in; + if (m_imperial_units) + value *= float(ObjectManipulation::mm_to_in); + float old_val = value; + + constexpr float UndefMinVal = -0.1f; + + const BoundingBoxf3 bbox = bounding_box(); + float mean_size = float((bbox.size().x() + bbox.size().y() + bbox.size().z()) / 9.0); + float min_size = value_in < 0.f ? UndefMinVal : 2.f; + if (m_imperial_units) { + mean_size *= float(ObjectManipulation::mm_to_in); + min_size *= float(ObjectManipulation::mm_to_in); + } + std::string format = value_in < 0.f ? UndefLabel : + m_imperial_units ? "%.4f " + _u8L("in") : "%.2f " + _u8L("mm"); + + m_imgui->slider_float(("##" + label).c_str(), &value, min_size, mean_size, format.c_str()); + value_in = value * float(m_imperial_units ? ObjectManipulation::in_to_mm : 1.0); + + ImGui::SameLine(m_label_width + m_control_width + 3); + ImGui::PushItemWidth(m_control_width * 0.3f); + + float old_tolerance, tolerance = old_tolerance = tolerance_in * 100.f; + std::string format_t = tolerance_in < 0.f ? UndefLabel : "%.f %%"; + float min_tolerance = tolerance_in < 0.f ? UndefMinVal : 0.f; + + m_imgui->slider_float(("##tolerance_" + label).c_str(), &tolerance, min_tolerance, 20.f, format_t.c_str(), 1.f, true, _L("Tolerance")); + tolerance_in = tolerance * 0.01f; + + return !is_approx(old_val, value) || !is_approx(old_tolerance, tolerance); +} + +void GLGizmoCut3D::render_move_center_input(int axis) +{ + m_imgui->text(m_axis_names[axis]+":"); + ImGui::SameLine(); + ImGui::PushItemWidth(0.3f*m_control_width); + + Vec3d move = m_plane_center; + double in_val, value = in_val = move[axis]; + if (m_imperial_units) + value *= ObjectManipulation::mm_to_in; + ImGui::InputDouble(("##move_" + m_axis_names[axis]).c_str(), &value, 0.0, 0.0, "%.2f", ImGuiInputTextFlags_CharsDecimal); + ImGui::SameLine(); + + double val = value * (m_imperial_units ? ObjectManipulation::in_to_mm : 1.0); + + if (in_val != val) { + move[axis] = val; + set_center(move); + } +} + +bool GLGizmoCut3D::render_connect_type_radio_button(CutConnectorType type) +{ + ImGui::SameLine(type == CutConnectorType::Plug ? m_label_width : 2*m_label_width); + ImGui::PushItemWidth(m_control_width); + if (m_imgui->radio_button(m_connector_types[size_t(type)], m_connector_type == type)) { + m_connector_type = type; + update_connector_shape(); + return true; + } + return false; +} + +void GLGizmoCut3D::render_connect_mode_radio_button(CutConnectorMode mode) +{ + ImGui::SameLine(mode == CutConnectorMode::Auto ? m_label_width : 2*m_label_width); + ImGui::PushItemWidth(m_control_width); + if (m_imgui->radio_button(m_connector_modes[int(mode)], m_connector_mode == mode)) + m_connector_mode = mode; +} + +bool GLGizmoCut3D::render_reset_button(const std::string& label_id, const std::string& tooltip) const +{ + const ImGuiStyle& style = ImGui::GetStyle(); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, { 1, style.ItemSpacing.y }); + + ImGui::PushStyleColor(ImGuiCol_Button, { 0.25f, 0.25f, 0.25f, 0.0f }); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, { 0.4f, 0.4f, 0.4f, 1.0f }); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, { 0.4f, 0.4f, 0.4f, 1.0f }); + + std::string btn_label; + btn_label += ImGui::RevertButton; + const bool revert = ImGui::Button((btn_label +"##" + label_id).c_str()); + + ImGui::PopStyleColor(3); + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(tooltip.c_str(), ImGui::GetFontSize() * 20.0f); + + ImGui::PopStyleVar(); + + return revert; +} + +static Vec2d ndc_to_ss(const Vec3d& ndc, const std::array& viewport) { + const double half_w = 0.5 * double(viewport[2]); + const double half_h = 0.5 * double(viewport[3]); + return { half_w * ndc.x() + double(viewport[0]) + half_w, half_h * ndc.y() + double(viewport[1]) + half_h }; +}; +static Vec3d clip_to_ndc(const Vec4d& clip) { + return Vec3d(clip.x(), clip.y(), clip.z()) / clip.w(); +} +static Vec4d world_to_clip(const Vec3d& world, const Matrix4d& projection_view_matrix) { + return projection_view_matrix * Vec4d(world.x(), world.y(), world.z(), 1.0); +} +static Vec2d world_to_ss(const Vec3d& world, const Matrix4d& projection_view_matrix, const std::array& viewport) { + return ndc_to_ss(clip_to_ndc(world_to_clip(world, projection_view_matrix)), viewport); +} + +static wxString get_label(Vec3d vec) +{ + wxString str = "x=" + double_to_string(vec.x(), 2) + + ", y=" + double_to_string(vec.y(), 2) + + ", z=" + double_to_string(vec.z(), 2); + return str; +} + +static wxString get_label(Vec2d vec) +{ + wxString str = "x=" + double_to_string(vec.x(), 2) + + ", y=" + double_to_string(vec.y(), 2); + return str; +} + +void GLGizmoCut3D::render_cut_plane() +{ + if (cut_line_processing()) return; - const BoundingBoxf3 box = bounding_box(); - m_max_z = box.max.z(); - m_start_z = m_cut_z; - m_drag_pos = m_grabbers[m_hover_id].center; - m_drag_center = box.center(); - m_drag_center.z() = m_cut_z; -} + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; -void GLGizmoCut::on_dragging(const UpdateData &data) -{ - assert(m_hover_id != -1); - set_cut_z(m_start_z + calc_projection(data.mouse_ray)); -} - -void GLGizmoCut::on_render() -{ - const BoundingBoxf3 box = bounding_box(); - Vec3d plane_center = box.center(); - plane_center.z() = m_cut_z; - m_max_z = box.max.z(); - set_cut_z(m_cut_z); - - update_contours(); - - const float min_x = box.min.x() - Margin; - const float max_x = box.max.x() + Margin; - const float min_y = box.min.y() - Margin; - const float max_y = box.max.y() + Margin; glsafe(::glEnable(GL_DEPTH_TEST)); glsafe(::glDisable(GL_CULL_FACE)); glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_GL_CORE_PROFILE - const Vec3d diff = plane_center - m_old_center; - // Z changed when move with cut plane - // X and Y changed when move with cutted object - bool is_changed = std::abs(diff.x()) > EPSILON || std::abs(diff.y()) > EPSILON || std::abs(diff.z()) > EPSILON; -#endif // ENABLE_GL_CORE_PROFILE + shader->start_using(); - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader != nullptr) { - shader->start_using(); -#if !ENABLE_GL_CORE_PROFILE - const Vec3d diff = plane_center - m_old_center; - // Z changed when move with cut plane - // X and Y changed when move with cutted object - bool is_changed = std::abs(diff.x()) > EPSILON || - std::abs(diff.y()) > EPSILON || - std::abs(diff.z()) > EPSILON; -#endif // !ENABLE_GL_CORE_PROFILE - m_old_center = plane_center; + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d view_model_matrix = camera.get_view_matrix() * translation_transform(m_plane_center) * m_rotation_m; - if (!m_plane.is_initialized() || is_changed) { - m_plane.reset(); + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; - init_data.color = { 0.8f, 0.8f, 0.8f, 0.5f }; - init_data.reserve_vertices(4); - init_data.reserve_indices(6); + if (can_perform_cut()) +// m_plane.set_color({ 0.8f, 0.8f, 0.8f, 0.5f }); + m_plane.set_color({ 0.9f, 0.9f, 0.9f, 0.5f }); + else + m_plane.set_color({ 1.0f, 0.8f, 0.8f, 0.5f }); + m_plane.render(); - // vertices - init_data.add_vertex(Vec3f(min_x, min_y, plane_center.z())); - init_data.add_vertex(Vec3f(max_x, min_y, plane_center.z())); - init_data.add_vertex(Vec3f(max_x, max_y, plane_center.z())); - init_data.add_vertex(Vec3f(min_x, max_y, plane_center.z())); + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glDisable(GL_BLEND)); - // indices - init_data.add_triangle(0, 1, 2); - init_data.add_triangle(2, 3, 0); - - m_plane.init_from(std::move(init_data)); - } - - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("view_model_matrix", camera.get_view_matrix()); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - - m_plane.render(); -#else - // Draw the cutting plane - ::glBegin(GL_QUADS); - ::glColor4f(0.8f, 0.8f, 0.8f, 0.5f); - ::glVertex3f(min_x, min_y, plane_center.z()); - ::glVertex3f(max_x, min_y, plane_center.z()); - ::glVertex3f(max_x, max_y, plane_center.z()); - ::glVertex3f(min_x, max_y, plane_center.z()); - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_GL_CORE_PROFILE - shader->stop_using(); - } - - shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); - if (shader != nullptr) { - shader->start_using(); - - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("view_model_matrix", camera.get_view_matrix()); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - const std::array& viewport = camera.get_viewport(); - shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); - shader->set_uniform("width", 0.5f); - shader->set_uniform("gap_size", 0.0f); -#endif // ENABLE_GL_CORE_PROFILE - - glsafe(::glEnable(GL_CULL_FACE)); - glsafe(::glDisable(GL_BLEND)); - - // Draw the grabber and the connecting line - m_grabbers[0].center = plane_center; - m_grabbers[0].center.z() = plane_center.z() + Offset; - - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); - -#if ENABLE_GL_CORE_PROFILE - if (!OpenGLManager::get_gl_info().is_core_profile()) -#endif // ENABLE_GL_CORE_PROFILE - glsafe(::glLineWidth(m_hover_id != -1 ? 2.0f : 1.5f)); -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_grabber_connection.is_initialized() || is_changed) { - m_grabber_connection.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.color = ColorRGBA::YELLOW(); - init_data.reserve_vertices(2); - init_data.reserve_indices(2); - - // vertices - init_data.add_vertex((Vec3f)plane_center.cast()); - init_data.add_vertex((Vec3f)m_grabbers[0].center.cast()); - - // indices - init_data.add_line(0, 1); - - m_grabber_connection.init_from(std::move(init_data)); - } - - m_grabber_connection.render(); - -#if ENABLE_GL_CORE_PROFILE - shader->set_uniform("view_model_matrix", camera.get_view_matrix()* Geometry::assemble_transform(m_cut_contours.shift)); - m_cut_contours.contours.render(); -#endif // ENABLE_GL_CORE_PROFILE - - shader->stop_using(); - } - - shader = wxGetApp().get_shader("gouraud_light"); -#else - glsafe(::glColor3f(1.0, 1.0, 0.0)); - ::glBegin(GL_LINES); - ::glVertex3dv(plane_center.data()); - ::glVertex3dv(m_grabbers[0].center.data()); - glsafe(::glEnd()); - - GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - if (shader != nullptr) { - shader->start_using(); - shader->set_uniform("emission_factor", 0.1f); - - m_grabbers[0].color = GRABBER_COLOR; - m_grabbers[0].render(m_hover_id == 0, float((box.size().x() + box.size().y() + box.size().z()) / 3.0)); - - shader->stop_using(); - } - -#if !ENABLE_GL_CORE_PROFILE -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader = wxGetApp().get_shader("flat"); - if (shader != nullptr) { - shader->start_using(); - - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("view_model_matrix", camera.get_view_matrix()* Geometry::assemble_transform(m_cut_contours.shift)); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#else - glsafe(::glPushMatrix()); - glsafe(::glTranslated(m_cut_contours.shift.x(), m_cut_contours.shift.y(), m_cut_contours.shift.z())); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glLineWidth(2.0f)); - m_cut_contours.contours.render(); -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); - } -#else - glsafe(::glPopMatrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#endif // !ENABLE_GL_CORE_PROFILE + shader->stop_using(); } -#if ENABLE_RAYCAST_PICKING -void GLGizmoCut::on_register_raycasters_for_picking() +static float get_grabber_mean_size(const BoundingBoxf3& bb) { + return float((bb.size().x() + bb.size().y() + bb.size().z()) / 3.0); +} + +void GLGizmoCut3D::render_model(GLModel& model, const ColorRGBA& color, Transform3d view_model_matrix) +{ + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader) { + shader->start_using(); + + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", wxGetApp().plater()->get_camera().get_projection_matrix()); + + model.set_color(color); + model.render(); + + shader->stop_using(); + } +} + +void GLGizmoCut3D::render_line(GLModel& line_model, const ColorRGBA& color, Transform3d view_model_matrix, float width) +{ + GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); + if (shader) { + shader->start_using(); + + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", wxGetApp().plater()->get_camera().get_projection_matrix()); + shader->set_uniform("width", width); + + line_model.set_color(color); + line_model.render(); + + shader->stop_using(); + } +} + +void GLGizmoCut3D::render_rotation_snapping(Axis axis, const ColorRGBA& color) +{ + GLShaderProgram* line_shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); + if (!line_shader) + return; + + const Camera& camera = wxGetApp().plater()->get_camera(); + Transform3d view_model_matrix = camera.get_view_matrix() * translation_transform(m_plane_center) * m_start_dragging_m; + + if (axis == X) + view_model_matrix = view_model_matrix * rotation_transform(0.5 * PI * Vec3d::UnitY()) * rotation_transform(-PI * Vec3d::UnitZ()); + else + view_model_matrix = view_model_matrix * rotation_transform(-0.5 * PI * Vec3d::UnitZ()) * rotation_transform(-0.5 * PI * Vec3d::UnitY()); + + line_shader->start_using(); + line_shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + line_shader->set_uniform("view_model_matrix", view_model_matrix); + line_shader->set_uniform("width", 0.25f); + + m_circle.render(); + m_scale.render(); + m_snap_radii.render(); + m_reference_radius.render(); + if (m_dragging) { + line_shader->set_uniform("width", 1.5f); + m_angle_arc.set_color(color); + m_angle_arc.render(); + } + + line_shader->stop_using(); +} + +void GLGizmoCut3D::render_grabber_connection(const ColorRGBA& color, Transform3d view_matrix) +{ + const Transform3d line_view_matrix = view_matrix * scale_transform(Vec3d(1.0, 1.0, m_grabber_connection_len)); + + render_line(m_grabber_connection, color, line_view_matrix, 0.2f); +}; + +void GLGizmoCut3D::render_cut_plane_grabbers() +{ + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + + ColorRGBA color = m_hover_id == Z ? complementary(GRABBER_COLOR) : GRABBER_COLOR; + + const Transform3d view_matrix = wxGetApp().plater()->get_camera().get_view_matrix() * translation_transform(m_plane_center) * m_rotation_m; + + const Grabber& grabber = m_grabbers.front(); + const float mean_size = get_grabber_mean_size(bounding_box()); + + double size = m_dragging && m_hover_id == Z ? double(grabber.get_dragging_half_size(mean_size)) : double(grabber.get_half_size(mean_size)); + + Vec3d cone_scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); + Vec3d offset = 1.25 * size * Vec3d::UnitZ(); + + // render Z grabber + + if (!m_dragging && m_hover_id < 0) + render_grabber_connection(color, view_matrix); + render_model(m_sphere.model, color, view_matrix * scale_transform(size)); + + if ((!m_dragging && m_hover_id < 0) || m_hover_id == Z) + { + const BoundingBoxf3 tbb = transformed_bounding_box(m_plane_center); + if (tbb.min.z() <= 0.0) + render_model(m_cone.model, color, view_matrix * assemble_transform(-offset, PI * Vec3d::UnitX(), cone_scale)); + + if (tbb.max.z() >= 0.0) + render_model(m_cone.model, color, view_matrix * assemble_transform(offset, Vec3d::Zero(), cone_scale)); + } + + // render top sphere for X/Y grabbers + + if ((!m_dragging && m_hover_id < 0) || m_hover_id == X || m_hover_id == Y) + { + size = m_dragging ? double(grabber.get_dragging_half_size(mean_size)) : double(grabber.get_half_size(mean_size)); + color = m_hover_id == Y ? complementary(ColorRGBA::GREEN()) : + m_hover_id == X ? complementary(ColorRGBA::RED()) : ColorRGBA::GRAY(); + render_model(m_sphere.model, color, view_matrix * assemble_transform(m_grabber_connection_len * Vec3d::UnitZ(), Vec3d::Zero(), size * Vec3d::Ones())); + } + + // render X grabber + + if ((!m_dragging && m_hover_id < 0) || m_hover_id == X) + { + size = m_dragging && m_hover_id == X ? double(grabber.get_dragging_half_size(mean_size)) : double(grabber.get_half_size(mean_size)); + cone_scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); + color = m_hover_id == X ? complementary(ColorRGBA::RED()) : ColorRGBA::RED(); + + if (m_hover_id == X) { + render_grabber_connection(color, view_matrix); + render_rotation_snapping(X, color); + } + + offset = Vec3d(0.0, 1.25 * size, m_grabber_connection_len); + render_model(m_cone.model, color, view_matrix * assemble_transform(offset, -0.5 * PI * Vec3d::UnitX(), cone_scale)); + offset = Vec3d(0.0, -1.25 * size, m_grabber_connection_len); + render_model(m_cone.model, color, view_matrix * assemble_transform(offset, 0.5 * PI * Vec3d::UnitX(), cone_scale)); + } + + // render Y grabber + + if ((!m_dragging && m_hover_id < 0) || m_hover_id == Y) + { + size = m_dragging && m_hover_id == Y ? double(grabber.get_dragging_half_size(mean_size)) : double(grabber.get_half_size(mean_size)); + cone_scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); + color = m_hover_id == Y ? complementary(ColorRGBA::GREEN()) : ColorRGBA::GREEN(); + + if (m_hover_id == Y) { + render_grabber_connection(color, view_matrix); + render_rotation_snapping(Y, color); + } + + offset = Vec3d(1.25 * size, 0.0, m_grabber_connection_len); + render_model(m_cone.model, color, view_matrix * assemble_transform(offset, 0.5 * PI * Vec3d::UnitY(), cone_scale)); + offset = Vec3d(-1.25 * size, 0.0, m_grabber_connection_len); + render_model(m_cone.model, color, view_matrix * assemble_transform(offset, -0.5 * PI * Vec3d::UnitY(), cone_scale)); + } +} + +void GLGizmoCut3D::render_cut_line() +{ + if (!cut_line_processing() || m_line_end == Vec3d::Zero()) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + + m_cut_line.reset(); + m_cut_line.init_from(its_make_line((Vec3f)m_line_beg.cast(), (Vec3f)m_line_end.cast())); + + render_line(m_cut_line, GRABBER_COLOR, wxGetApp().plater()->get_camera().get_view_matrix(), 0.25f); +} + +bool GLGizmoCut3D::on_init() +{ + m_grabbers.emplace_back(); + m_shortcut_key = WXK_CONTROL_C; + + // initiate info shortcuts + const wxString ctrl = GUI::shortkey_ctrl_prefix(); + const wxString alt = GUI::shortkey_alt_prefix(); + const wxString shift = "Shift+"; + + m_shortcuts.push_back(std::make_pair(_L("Left click"), _L("Add connector"))); + m_shortcuts.push_back(std::make_pair(_L("Right click"), _L("Remove connector"))); + m_shortcuts.push_back(std::make_pair(_L("Drag"), _L("Move connector"))); + m_shortcuts.push_back(std::make_pair(shift + _L("Left click"), _L("Add connector to selection"))); + m_shortcuts.push_back(std::make_pair(alt + _L("Left click"), _L("Remove connector from selection"))); + m_shortcuts.push_back(std::make_pair(ctrl + "A", _L("Select all connectors"))); + + return true; +} + +void GLGizmoCut3D::on_load(cereal::BinaryInputArchive& ar) +{ + ar( m_keep_upper, m_keep_lower, m_rotate_lower, m_rotate_upper, m_hide_cut_plane, m_mode, m_connectors_editing,//m_selected, + // m_connector_depth_ratio, m_connector_size, m_connector_mode, m_connector_type, m_connector_style, m_connector_shape_id, + m_ar_plane_center, m_rotation_m); + + set_center_pos(m_ar_plane_center, true); + + force_update_clipper_on_render = true; + + m_parent.request_extra_frame(); +} + +void GLGizmoCut3D::on_save(cereal::BinaryOutputArchive& ar) const +{ + ar( m_keep_upper, m_keep_lower, m_rotate_lower, m_rotate_upper, m_hide_cut_plane, m_mode, m_connectors_editing,//m_selected, + // m_connector_depth_ratio, m_connector_size, m_connector_mode, m_connector_type, m_connector_style, m_connector_shape_id, + m_ar_plane_center, m_start_dragging_m); +} + +std::string GLGizmoCut3D::on_get_name() const +{ + return _u8L("Cut"); +} + +void GLGizmoCut3D::on_set_state() +{ + if (m_state == On) { + update_bb(); + m_connectors_editing = !m_selected.empty(); + + // initiate archived values + m_ar_plane_center = m_plane_center; + m_start_dragging_m = m_rotation_m; + + m_parent.request_extra_frame(); + } + else { + m_c->object_clipper()->release(); + m_selected.clear(); + } + force_update_clipper_on_render = m_state == On; +} + +void GLGizmoCut3D::on_register_raycasters_for_picking() +{ + assert(m_raycasters.empty()); // the gizmo grabbers are rendered on top of the scene, so the raytraced picker should take it into account m_parent.set_raycaster_gizmos_on_top(true); + + init_picking_models(); + + if (m_connectors_editing) { + if (CommonGizmosDataObjects::SelectionInfo* si = m_c->selection_info()) { + const CutConnectors& connectors = si->model_object()->cut_connectors; + for (int i = 0; i < int(connectors.size()); ++i) + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i + m_connectors_group_id, *(m_shapes[connectors[i].attribs]).mesh_raycaster, Transform3d::Identity())); + } + } + else { + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, X, *m_cone.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, X, *m_cone.mesh_raycaster, Transform3d::Identity())); + + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Y, *m_cone.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Y, *m_cone.mesh_raycaster, Transform3d::Identity())); + + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Z, *m_sphere.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Z, *m_cone.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Z, *m_cone.mesh_raycaster, Transform3d::Identity())); + } + + update_raycasters_for_picking_transform(); } -void GLGizmoCut::on_unregister_raycasters_for_picking() +void GLGizmoCut3D::on_unregister_raycasters_for_picking() { + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo); + m_raycasters.clear(); + // the gizmo grabbers are rendered on top of the scene, so the raytraced picker should take it into account m_parent.set_raycaster_gizmos_on_top(false); } -#else -void GLGizmoCut::on_render_for_picking() + +void GLGizmoCut3D::update_raycasters_for_picking() { - glsafe(::glDisable(GL_DEPTH_TEST)); - render_grabbers_for_picking(m_parent.get_selection().get_bounding_box()); + on_unregister_raycasters_for_picking(); + on_register_raycasters_for_picking(); } -#endif // ENABLE_RAYCAST_PICKING -void GLGizmoCut::on_render_input_window(float x, float y, float bottom_limit) +void GLGizmoCut3D::set_volumes_picking_state(bool state) { - static float last_y = 0.0f; - static float last_h = 0.0f; - - m_imgui->begin(_L("Cut"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - - const bool imperial_units = wxGetApp().app_config->get("use_inches") == "1"; - - // adjust window position to avoid overlap the view toolbar - const float win_h = ImGui::GetWindowHeight(); - y = std::min(y, bottom_limit - win_h); - ImGui::SetWindowPos(ImVec2(x, y), ImGuiCond_Always); - 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; + std::vector>* raycasters = m_parent.get_raycasters_for_picking(SceneRaycaster::EType::Volume); + if (raycasters != nullptr) { + const Selection& selection = m_parent.get_selection(); + const Selection::IndicesList ids = selection.get_volume_idxs(); + for (unsigned int id : ids) { + const GLVolume* v = selection.get_volume(id); + auto it = std::find_if(raycasters->begin(), raycasters->end(), [v](std::shared_ptr item) { return item->get_raycaster() == v->mesh_raycaster.get(); }); + if (it != raycasters->end()) + (*it)->set_active(state); + } } - - ImGui::AlignTextToFramePadding(); - m_imgui->text("Z"); - ImGui::SameLine(); - ImGui::PushItemWidth(m_imgui->get_style_scaling() * 150.0f); - - double cut_z = m_cut_z; - if (imperial_units) - cut_z *= ObjectManipulation::mm_to_in; - ImGui::InputDouble("", &cut_z, 0.0f, 0.0f, "%.2f", ImGuiInputTextFlags_CharsDecimal); - - ImGui::SameLine(); - m_imgui->text(imperial_units ? _L("in") : _L("mm")); - - m_cut_z = cut_z * (imperial_units ? ObjectManipulation::in_to_mm : 1.0); - - ImGui::Separator(); - - m_imgui->checkbox(_L("Keep upper part"), m_keep_upper); - m_imgui->checkbox(_L("Keep lower part"), m_keep_lower); - m_imgui->checkbox(_L("Rotate lower part upwards"), m_rotate_lower); - - ImGui::Separator(); - - m_imgui->disabled_begin((!m_keep_upper && !m_keep_lower) || m_cut_z <= 0.0 || m_max_z <= m_cut_z); - const bool cut_clicked = m_imgui->button(_L("Perform cut")); - m_imgui->disabled_end(); - - m_imgui->end(); - - if (cut_clicked && (m_keep_upper || m_keep_lower)) - perform_cut(m_parent.get_selection()); } -void GLGizmoCut::set_cut_z(double cut_z) +void GLGizmoCut3D::update_raycasters_for_picking_transform() { - // Clamp the plane to the object's bounding box - m_cut_z = std::clamp(cut_z, 0.0, m_max_z); -} + if (m_connectors_editing) { + CommonGizmosDataObjects::SelectionInfo* si = m_c->selection_info(); + if (!si) + return; + const ModelObject* mo = si->model_object(); + const CutConnectors& connectors = mo->cut_connectors; + if (connectors.empty()) + return; + auto inst_id = m_c->selection_info()->get_active_instance(); + if (inst_id < 0) + return; -void GLGizmoCut::perform_cut(const Selection& selection) -{ - const int instance_idx = selection.get_instance_idx(); - const int object_idx = selection.get_object_idx(); + const Vec3d& instance_offset = mo->instances[inst_id]->get_offset(); + const double sla_shift = double(m_c->selection_info()->get_sla_shift()); - wxCHECK_RET(instance_idx >= 0 && object_idx >= 0, "GLGizmoCut: Invalid object selection"); + const ClippingPlane* cp = m_c->object_clipper()->get_clipping_plane(); + const Vec3d& normal = cp && cp->is_active() ? cp->get_normal() : m_clp_normal; - // m_cut_z is the distance from the bed. Subtract possible SLA elevation. - const GLVolume* first_glvolume = selection.get_first_volume(); - const double object_cut_z = m_cut_z - first_glvolume->get_sla_shift_z(); + for (size_t i = 0; i < connectors.size(); ++i) { + const CutConnector& connector = connectors[i]; - if (0.0 < object_cut_z && object_cut_z < m_max_z) - wxGetApp().plater()->cut(object_idx, instance_idx, object_cut_z, - only_if(m_keep_upper, ModelObjectCutAttribute::KeepUpper) | - only_if(m_keep_lower, ModelObjectCutAttribute::KeepLower) | - only_if(m_rotate_lower, ModelObjectCutAttribute::FlipLower)); + float height = connector.height; + // recalculate connector position to world position + Vec3d pos = connector.pos + instance_offset; + if (connector.attribs.type == CutConnectorType::Dowel && + connector.attribs.style == CutConnectorStyle::Prizm) { + pos -= height * normal; + height *= 2; + } + pos[Z] += sla_shift; + + const Transform3d scale_trafo = scale_transform(Vec3f(connector.radius, connector.radius, height).cast()); + m_raycasters[i]->set_transform(translation_transform(pos) * m_rotation_m * scale_trafo); + } + } else { - // the object is SLA-elevated and the plane is under it. + const Transform3d trafo = translation_transform(m_plane_center) * m_rotation_m; + + const BoundingBoxf3 box = bounding_box(); + const float mean_size = get_grabber_mean_size(box); + + double size = double(m_grabbers.front().get_half_size(mean_size)); + Vec3d scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); + + Vec3d offset = Vec3d(0.0, 1.25 * size, m_grabber_connection_len); + m_raycasters[0]->set_transform(trafo * assemble_transform(offset, -0.5 * PI * Vec3d::UnitX(), scale)); + offset = Vec3d(0.0, -1.25 * size, m_grabber_connection_len); + m_raycasters[1]->set_transform(trafo * assemble_transform(offset, 0.5 * PI * Vec3d::UnitX(), scale)); + + offset = Vec3d(1.25 * size, 0.0, m_grabber_connection_len); + m_raycasters[2]->set_transform(trafo * assemble_transform(offset, 0.5 * PI * Vec3d::UnitY(), scale)); + offset = Vec3d(-1.25 * size, 0.0, m_grabber_connection_len); + m_raycasters[3]->set_transform(trafo * assemble_transform(offset, -0.5 * PI * Vec3d::UnitY(), scale)); + + offset = 1.25 * size * Vec3d::UnitZ(); + m_raycasters[4]->set_transform(trafo * scale_transform(size)); + m_raycasters[5]->set_transform(trafo * assemble_transform(-offset, PI * Vec3d::UnitX(), scale)); + m_raycasters[6]->set_transform(trafo * assemble_transform(offset, Vec3d::Zero(), scale)); } } -double GLGizmoCut::calc_projection(const Linef3& mouse_ray) const +void GLGizmoCut3D::on_set_hover_id() { - double projection = 0.0; +} - const Vec3d starting_vec = m_drag_pos - m_drag_center; - const double len_starting_vec = starting_vec.norm(); - if (len_starting_vec != 0.0) { - const Vec3d mouse_dir = mouse_ray.unit_vector(); +bool GLGizmoCut3D::on_is_activable() const +{ + const Selection& selection = m_parent.get_selection(); + const int object_idx = selection.get_object_idx(); + if (object_idx < 0) + return false; + + bool is_dowel_object = false; + if (const ModelObject* mo = wxGetApp().plater()->model().objects[object_idx]; mo->is_cut()) { + int solid_connector_cnt = 0; + int connectors_cnt = 0; + for (const ModelVolume* volume : mo->volumes) { + if (volume->is_cut_connector()) { + connectors_cnt++; + if (volume->is_model_part()) + solid_connector_cnt++; + } + if (connectors_cnt > 1) + break; + } + is_dowel_object = connectors_cnt == 1 && solid_connector_cnt == 1; + } + + // This is assumed in GLCanvas3D::do_rotate, do not change this + // without updating that function too. + return selection.is_single_full_instance() && !is_dowel_object && !m_parent.is_layers_editing_enabled(); +} + +bool GLGizmoCut3D::on_is_selectable() const +{ + return wxGetApp().get_mode() != comSimple; +} + +Vec3d GLGizmoCut3D::mouse_position_in_local_plane(Axis axis, const Linef3& mouse_ray) const +{ + double half_pi = 0.5 * PI; + + Transform3d m = Transform3d::Identity(); + + switch (axis) + { + case X: + { + m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); + m.rotate(Eigen::AngleAxisd(-half_pi, Vec3d::UnitY())); + break; + } + case Y: + { + m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitY())); + m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); + break; + } + default: + case Z: + { + // no rotation applied + break; + } + } + + m = m * m_start_dragging_m.inverse(); + m.translate(-m_plane_center); + + return transform(mouse_ray, m).intersect_plane(0.0); +} + +void GLGizmoCut3D::dragging_grabber_z(const GLGizmoBase::UpdateData &data) +{ + Vec3d starting_box_center = m_plane_center - Vec3d::UnitZ(); // some Margin + rotate_vec3d_around_plane_center(starting_box_center); + + const Vec3d&starting_drag_position = m_plane_center; + double projection = 0.0; + + Vec3d starting_vec = starting_drag_position - starting_box_center; + if (starting_vec.norm() != 0.0) { + Vec3d mouse_dir = data.mouse_ray.unit_vector(); // finds the intersection of the mouse ray with the plane parallel to the camera viewport and passing throught the starting position // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebric form // in our case plane normal and ray direction are the same (orthogonal view) // when moving to perspective camera the negative z unit axis of the camera needs to be transformed in world space and used as plane normal - const Vec3d inters = mouse_ray.a + (m_drag_pos - mouse_ray.a).dot(mouse_dir) / mouse_dir.squaredNorm() * mouse_dir; + Vec3d inters = data.mouse_ray.a + (starting_drag_position - data.mouse_ray.a).dot(mouse_dir) / mouse_dir.squaredNorm() * mouse_dir; // vector from the starting position to the found intersection - const Vec3d inters_vec = inters - m_drag_pos; + Vec3d inters_vec = inters - starting_drag_position; + starting_vec.normalize(); // finds projection of the vector along the staring direction - projection = inters_vec.dot(starting_vec.normalized()); + projection = inters_vec.dot(starting_vec); } - return projection; + if (wxGetKeyState(WXK_SHIFT)) + projection = m_snap_step * (double)std::round(projection / m_snap_step); + + const Vec3d shift = starting_vec * projection; + + // move cut plane center + set_center(m_plane_center + shift); } -BoundingBoxf3 GLGizmoCut::bounding_box() const +void GLGizmoCut3D::dragging_grabber_xy(const GLGizmoBase::UpdateData &data) +{ + const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane((Axis)m_hover_id, data.mouse_ray)); + + const Vec2d orig_dir = Vec2d::UnitX(); + const Vec2d new_dir = mouse_pos.normalized(); + + const double two_pi = 2.0 * PI; + + double theta = ::acos(std::clamp(new_dir.dot(orig_dir), -1.0, 1.0)); + if (cross2(orig_dir, new_dir) < 0.0) + theta = two_pi - theta; + + const double len = mouse_pos.norm(); + // snap to coarse snap region + if (m_snap_coarse_in_radius <= len && len <= m_snap_coarse_out_radius) { + const double step = two_pi / double(SnapRegionsCount); + theta = step * std::round(theta / step); + } + // snap to fine snap region (scale) + else if (m_snap_fine_in_radius <= len && len <= m_snap_fine_out_radius) { + const double step = two_pi / double(ScaleStepsCount); + theta = step * std::round(theta / step); + } + + if (is_approx(theta, two_pi)) + theta = 0.0; + if (m_hover_id == X) + theta += 0.5 * PI; + + Vec3d rotation = Vec3d::Zero(); + rotation[m_hover_id] = theta; + m_rotation_m = m_start_dragging_m * rotation_transform(rotation); + + m_angle = theta; + while (m_angle > two_pi) + m_angle -= two_pi; + if (m_angle < 0.0) + m_angle += two_pi; + + update_clipper(); +} + +void GLGizmoCut3D::dragging_connector(const GLGizmoBase::UpdateData &data) +{ + CutConnectors& connectors = m_c->selection_info()->model_object()->cut_connectors; + std::pair pos_and_normal; + Vec3d pos_world; + + if (unproject_on_cut_plane(data.mouse_pos.cast(), pos_and_normal, pos_world)) { + connectors[m_hover_id - m_connectors_group_id].pos = pos_and_normal.first; + update_raycasters_for_picking_transform(); + } +} + +void GLGizmoCut3D::on_dragging(const UpdateData& data) +{ + if (m_hover_id < 0) + return; + if (m_hover_id == Z) + dragging_grabber_z(data); + else if (m_hover_id == X || m_hover_id == Y) + dragging_grabber_xy(data); + else if (m_hover_id >= m_connectors_group_id && m_connector_mode == CutConnectorMode::Manual) + dragging_connector(data); +} + +void GLGizmoCut3D::on_start_dragging() +{ + m_angle = 0.0; + if (m_hover_id >= m_connectors_group_id && m_connector_mode == CutConnectorMode::Manual) + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Move connector"), UndoRedo::SnapshotType::GizmoAction); + + if (m_hover_id == X || m_hover_id == Y) + m_start_dragging_m = m_rotation_m; +} + +void GLGizmoCut3D::on_stop_dragging() +{ + if (m_hover_id == X || m_hover_id == Y) { + m_angle_arc.reset(); + m_angle = 0.0; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Rotate cut plane"), UndoRedo::SnapshotType::GizmoAction); + m_start_dragging_m = m_rotation_m; + } + else if (m_hover_id == Z) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Move cut plane"), UndoRedo::SnapshotType::GizmoAction); + m_ar_plane_center = m_plane_center; + } +} + +void GLGizmoCut3D::set_center_pos(const Vec3d& center_pos, bool force/* = false*/) +{ + bool can_set_center_pos = force; + if (!can_set_center_pos) { + const BoundingBoxf3 tbb = transformed_bounding_box(center_pos); + if (tbb.max.z() > -1. && tbb.min.z() < 1.) + can_set_center_pos = true; + else { + const double old_dist = (m_bb_center - m_plane_center).norm(); + const double new_dist = (m_bb_center - center_pos).norm(); + // check if forcing is reasonable + if (new_dist < old_dist) + can_set_center_pos = true; + } + } + + if (can_set_center_pos) { + m_plane_center = center_pos; + m_center_offset = m_plane_center - m_bb_center; + } +} + +BoundingBoxf3 GLGizmoCut3D::bounding_box() const { BoundingBoxf3 ret; const Selection& selection = m_parent.get_selection(); const Selection::IndicesList& idxs = selection.get_volume_idxs(); - return selection.get_bounding_box(); - for (unsigned int i : idxs) { const GLVolume* volume = selection.get_volume(i); - if (!volume->is_modifier) + // respect just to the solid parts for FFF and ignore pad and supports for SLA + if (!volume->is_modifier && !volume->is_sla_pad() && !volume->is_sla_support()) ret.merge(volume->transformed_convex_hull_bounding_box()); } return ret; } -void GLGizmoCut::update_contours() +BoundingBoxf3 GLGizmoCut3D::transformed_bounding_box(const Vec3d& plane_center, bool revert_move /*= false*/) const { - const Selection& selection = m_parent.get_selection(); - const GLVolume* first_glvolume = selection.get_first_volume(); - const BoundingBoxf3& box = first_glvolume->transformed_convex_hull_bounding_box(); + // #ysFIXME !!! + BoundingBoxf3 ret; - const ModelObject* model_object = wxGetApp().model().objects[selection.get_object_idx()]; - const int instance_idx = selection.get_instance_idx(); - std::vector volumes_idxs = std::vector(model_object->volumes.size()); - std::vector volumes_trafos = std::vector(model_object->volumes.size()); - for (size_t i = 0; i < model_object->volumes.size(); ++i) { - volumes_idxs[i] = model_object->volumes[i]->id(); - volumes_trafos[i] = model_object->volumes[i]->get_matrix(); + const CommonGizmosDataObjects::SelectionInfo* sel_info = m_c->selection_info(); + if (!sel_info) + return ret; + const ModelObject* mo = sel_info->model_object(); + if (!mo) + return ret; + const int instance_idx = sel_info->get_active_instance(); + if (instance_idx < 0 || mo->instances.empty()) + return ret; + const ModelInstance* mi = mo->instances[instance_idx]; + + const Vec3d& instance_offset = mi->get_offset(); + Vec3d cut_center_offset = plane_center - instance_offset; + cut_center_offset[Z] -= sel_info->get_sla_shift(); + + const auto move = assemble_transform(-cut_center_offset); + const auto move2 = assemble_transform(plane_center); + + const auto cut_matrix = (revert_move ? move2 : Transform3d::Identity()) * m_rotation_m.inverse() * move; + + const Selection& selection = m_parent.get_selection(); + const Selection::IndicesList& idxs = selection.get_volume_idxs(); + for (unsigned int i : idxs) { + const GLVolume* volume = selection.get_volume(i); + // respect just to the solid parts for FFF and ignore pad and supports for SLA + if (!volume->is_modifier && !volume->is_sla_pad() && !volume->is_sla_support()) { + + const auto instance_matrix = assemble_transform( + Vec3d::Zero(), // don't apply offset + volume->get_instance_rotation().cwiseProduct(Vec3d(1.0, 1.0, 1.0)), + volume->get_instance_scaling_factor(), + volume->get_instance_mirror() + ); + + auto volume_trafo = instance_matrix * volume->get_volume_transformation().get_matrix(); + + ret.merge(volume->transformed_convex_hull_bounding_box(cut_matrix * volume_trafo)); + } + } + return ret; +} + +bool GLGizmoCut3D::update_bb() +{ + const BoundingBoxf3 box = bounding_box(); + if (m_max_pos != box.max || m_min_pos != box.min) { + + invalidate_cut_plane(); + + m_max_pos = box.max; + m_min_pos = box.min; + m_bb_center = box.center(); + if (box.contains(m_center_offset)) + set_center_pos(m_bb_center + m_center_offset, true); + else + set_center_pos(m_bb_center, true); + + m_radius = box.radius(); + m_grabber_connection_len = 0.75 * m_radius;// std::min(0.75 * m_radius, 35.0); + m_grabber_radius = m_grabber_connection_len * 0.85; + + m_snap_coarse_in_radius = m_grabber_radius / 3.0; + m_snap_coarse_out_radius = m_snap_coarse_in_radius * 2.; + m_snap_fine_in_radius = m_grabber_connection_len * 0.85; + m_snap_fine_out_radius = m_grabber_connection_len * 1.15; + + m_plane.reset(); + m_cone.reset(); + m_sphere.reset(); + m_grabber_connection.reset(); + m_circle.reset(); + m_scale.reset(); + m_snap_radii.reset(); + m_reference_radius.reset(); + + on_unregister_raycasters_for_picking(); + + clear_selection(); + if (CommonGizmosDataObjects::SelectionInfo* selection = m_c->selection_info()) + m_selected.resize(selection->model_object()->cut_connectors.size(), false); + + return true; + } + return false; +} + +void GLGizmoCut3D::init_picking_models() +{ + if (!m_cone.model.is_initialized()) { + indexed_triangle_set its = its_make_cone(1.0, 1.0, PI / 12.0); + m_cone.model.init_from(its); + m_cone.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); + } + if (!m_sphere.model.is_initialized()) { + indexed_triangle_set its = its_make_sphere(1.0, PI / 12.0); + m_sphere.model.init_from(its); + m_sphere.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); + } + if (m_shapes.empty()) + init_connector_shapes(); +} + +void GLGizmoCut3D::init_rendering_items() +{ + if (!m_grabber_connection.is_initialized()) + m_grabber_connection.init_from(its_make_line(Vec3f::Zero(), Vec3f::UnitZ())); + if (!m_circle.is_initialized()) + init_from_circle(m_circle, m_grabber_radius); + if (!m_scale.is_initialized()) + init_from_scale(m_scale, m_grabber_radius); + if (!m_snap_radii.is_initialized()) + init_from_snap_radii(m_snap_radii, m_grabber_radius); + if (!m_reference_radius.is_initialized()) { + m_reference_radius.init_from(its_make_line(Vec3f::Zero(), m_grabber_connection_len * Vec3f::UnitX())); + m_reference_radius.set_color(ColorRGBA::WHITE()); + } + if (!m_angle_arc.is_initialized() || m_angle != 0.0) + init_from_angle_arc(m_angle_arc, m_angle, m_grabber_connection_len); + + if (!m_plane.is_initialized() && !m_hide_cut_plane && !m_connectors_editing) { + if (m_cut_plane_as_circle) + m_plane.init_from(its_make_frustum_dowel(2. * m_radius, 0.3, 180)); + else + m_plane.init_from(its_make_square_plane(float(m_radius))); + } +} + +void GLGizmoCut3D::render_clipper_cut() +{ + if (! m_connectors_editing) + ::glDisable(GL_DEPTH_TEST); + m_c->object_clipper()->render_cut(); + if (! m_connectors_editing) + ::glEnable(GL_DEPTH_TEST); +} + +void GLGizmoCut3D::on_render() +{ + if (update_bb() || force_update_clipper_on_render) { + update_clipper_on_render(); + m_c->object_clipper()->set_behavior(m_connectors_editing, m_connectors_editing, 0.4); } - bool trafos_match = std::equal(volumes_trafos.begin(), volumes_trafos.end(), - m_cut_contours.volumes_trafos.begin(), m_cut_contours.volumes_trafos.end(), - [](const Transform3d& a, const Transform3d& b) { return a.isApprox(b); }); + init_picking_models(); - if (0.0 < m_cut_z && m_cut_z < m_max_z) { - if (m_cut_contours.cut_z != m_cut_z || m_cut_contours.object_id != model_object->id() || - m_cut_contours.instance_idx != instance_idx || m_cut_contours.volumes_idxs != volumes_idxs || - !trafos_match) { - m_cut_contours.cut_z = m_cut_z; + init_rendering_items(); - if (m_cut_contours.object_id != model_object->id() || m_cut_contours.volumes_idxs != volumes_idxs || !trafos_match) - m_cut_contours.mesh = model_object->raw_mesh(); + render_connectors(); - m_cut_contours.position = box.center(); - m_cut_contours.shift = Vec3d::Zero(); - m_cut_contours.object_id = model_object->id(); - m_cut_contours.instance_idx = instance_idx; - m_cut_contours.volumes_idxs = volumes_idxs; - m_cut_contours.volumes_trafos = volumes_trafos; - m_cut_contours.contours.reset(); + render_clipper_cut(); - MeshSlicingParams slicing_params; - slicing_params.trafo = first_glvolume->get_instance_transformation().get_matrix(); - slicing_params.trafo.pretranslate(Vec3d(0., 0., first_glvolume->get_sla_shift_z())); + if (!m_hide_cut_plane && !m_connectors_editing) { + render_cut_plane(); + render_cut_plane_grabbers(); + } - const Polygons polys = slice_mesh(m_cut_contours.mesh.its, m_cut_z, slicing_params); - if (!polys.empty()) { - m_cut_contours.contours.init_from(polys, static_cast(m_cut_z)); -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_cut_contours.contours.set_color(ColorRGBA::WHITE()); -#else - m_cut_contours.contours.set_color(-1, { 1.0f, 1.0f, 1.0f, 1.0f }); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL + render_cut_line(); + + m_selection_rectangle.render(m_parent); +} + +void GLGizmoCut3D::render_debug_input_window() +{ + m_imgui->begin(wxString("DEBUG")); + + static bool hide_clipped = false; + static bool fill_cut = false; + static float contour_width = 0.4f; + + m_imgui->checkbox(_L("Hide cut plane and grabbers"), m_hide_cut_plane); + if (m_imgui->checkbox("hide_clipped", hide_clipped) && !hide_clipped) + m_clp_normal = m_c->object_clipper()->get_clipping_plane()->get_normal(); + m_imgui->checkbox("fill_cut", fill_cut); + m_imgui->slider_float("contour_width", &contour_width, 0.f, 3.f); + if (auto oc = m_c->object_clipper()) + oc->set_behavior(hide_clipped || m_connectors_editing, fill_cut || m_connectors_editing, double(contour_width)); + + ImGui::Separator(); + + if (m_imgui->checkbox(_L("Render cut plane as circle"), m_cut_plane_as_circle)) + m_plane.reset(); + + m_imgui->end(); +} + +void GLGizmoCut3D::adjust_window_position(float x, float y, float bottom_limit) +{ + 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); + + ImGui::SetWindowPos(ImVec2(x, y), ImGuiCond_Always); + + if (!is_approx(last_h, win_h) || !is_approx(last_y, y)) { + // ask canvas for another frame to render the window in the correct position + m_imgui->set_requires_extra_frame(); + if (!is_approx(last_h, win_h)) + last_h = win_h; + if (!is_approx(last_y, y)) + last_y = y; + } +} + +void GLGizmoCut3D::unselect_all_connectors() +{ + std::fill(m_selected.begin(), m_selected.end(), false); + m_selected_count = 0; + validate_connector_settings(); +} + +void GLGizmoCut3D::select_all_connectors() +{ + std::fill(m_selected.begin(), m_selected.end(), true); + m_selected_count = int(m_selected.size()); +} + +void GLGizmoCut3D::render_shortcuts() +{ + if (m_imgui->button("? " + (m_show_shortcuts ? wxString(ImGui::CollapseBtn) : wxString(ImGui::ExpandBtn)))) + m_show_shortcuts = !m_show_shortcuts; + + if (m_show_shortcuts) + for (const auto&shortcut : m_shortcuts ){ + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, shortcut.first); + ImGui::SameLine(m_label_width); + m_imgui->text(shortcut.second); + } +} + +void GLGizmoCut3D::apply_selected_connectors(std::function apply_fn) +{ + for (size_t idx = 0; idx < m_selected.size(); idx++) + if (m_selected[idx]) + apply_fn(idx); + + update_raycasters_for_picking_transform(); +} + +void GLGizmoCut3D::render_connectors_input_window(CutConnectors &connectors) +{ + // add shortcuts panel + render_shortcuts(); + + // Connectors section + + ImGui::Separator(); + + // WIP : Auto : Need to implement + // m_imgui->text(_L("Mode")); + // render_connect_mode_radio_button(CutConnectorMode::Auto); + // render_connect_mode_radio_button(CutConnectorMode::Manual); + + ImGui::AlignTextToFramePadding(); + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Connectors")); + + m_imgui->disabled_begin(connectors.empty()); + ImGui::SameLine(m_label_width); + if (render_reset_button("connectors", _u8L("Remove connectors"))) + reset_connectors(); + m_imgui->disabled_end(); + + m_imgui->text(_L("Type")); + bool type_changed = render_connect_type_radio_button(CutConnectorType::Plug); + type_changed |= render_connect_type_radio_button(CutConnectorType::Dowel); + if (type_changed) + apply_selected_connectors([this, &connectors] (size_t idx) { connectors[idx].attribs.type = CutConnectorType(m_connector_type); }); + + m_imgui->disabled_begin(m_connector_type == CutConnectorType::Dowel); + if (type_changed && m_connector_type == CutConnectorType::Dowel) { + m_connector_style = size_t(CutConnectorStyle::Prizm); + apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.style = CutConnectorStyle(m_connector_style); }); + } + if (render_combo(_u8L("Style"), m_connector_styles, m_connector_style)) + apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.style = CutConnectorStyle(m_connector_style); }); + m_imgui->disabled_end(); + + if (render_combo(_u8L("Shape"), m_connector_shapes, m_connector_shape_id)) + apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.shape = CutConnectorShape(m_connector_shape_id); }); + + if (render_slider_double_input(_u8L("Depth ratio"), m_connector_depth_ratio, m_connector_depth_ratio_tolerance)) + apply_selected_connectors([this, &connectors](size_t idx) { + if (m_connector_depth_ratio > 0) + connectors[idx].height = m_connector_depth_ratio; + if (m_connector_depth_ratio_tolerance >= 0) + connectors[idx].height_tolerance = m_connector_depth_ratio_tolerance; + }); + + if (render_slider_double_input(_u8L("Size"), m_connector_size, m_connector_size_tolerance)) + apply_selected_connectors([this, &connectors](size_t idx) { + if (m_connector_size > 0) + connectors[idx].radius = 0.5f * m_connector_size; + if (m_connector_size_tolerance >= 0) + connectors[idx].radius_tolerance = m_connector_size_tolerance; + }); + + ImGui::Separator(); + + if (m_imgui->button(_L("Confirm connectors"))) { + m_clp_normal = m_c->object_clipper()->get_clipping_plane()->get_normal(); + unselect_all_connectors(); + set_connectors_editing(false); + } +} + +void GLGizmoCut3D::render_build_size() +{ + double koef = m_imperial_units ? ObjectManipulation::mm_to_in : 1.0; + wxString unit_str = " " + (m_imperial_units ? _L("in") : _L("mm")); + const BoundingBoxf3 tbb = transformed_bounding_box(m_plane_center); + + Vec3d tbb_sz = tbb.size(); + wxString size = "X: " + double_to_string(tbb_sz.x() * koef, 2) + unit_str + + ", Y: " + double_to_string(tbb_sz.y() * koef, 2) + unit_str + + ", Z: " + double_to_string(tbb_sz.z() * koef, 2) + unit_str; + + ImGui::AlignTextToFramePadding(); + m_imgui->text(_L("Build size")); + ImGui::SameLine(m_label_width); + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, size); +} + +void GLGizmoCut3D::reset_cut_plane() +{ + set_center(bounding_box().center()); + m_rotation_m = Transform3d::Identity(); + m_angle_arc.reset(); + update_clipper(); +} + +void GLGizmoCut3D::invalidate_cut_plane() +{ + m_rotation_m = Transform3d::Identity(); + m_plane_center = Vec3d::Zero(); + m_min_pos = Vec3d::Zero(); + m_max_pos = Vec3d::Zero(); + m_bb_center = Vec3d::Zero(); + m_center_offset = Vec3d::Zero(); +} + +void GLGizmoCut3D::set_connectors_editing(bool connectors_editing) +{ + m_connectors_editing = connectors_editing; + update_raycasters_for_picking(); + + m_parent.request_extra_frame(); +} + +void GLGizmoCut3D::render_cut_plane_input_window(CutConnectors &connectors) +{ + // WIP : cut plane mode + // render_combo(_u8L("Mode"), m_modes, m_mode); + + if (m_mode == size_t(CutMode::cutPlanar)) { + ImGui::AlignTextToFramePadding(); + m_imgui->text(wxString(ImGui::InfoMarkerSmall)); + ImGui::SameLine(); + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Hold SHIFT key and connect some two points of an object to cut by line")); + + ImGui::Separator(); + + render_build_size(); + + ImGui::AlignTextToFramePadding(); + m_imgui->text(_L("Cut position: ")); + ImGui::SameLine(m_label_width); + render_move_center_input(Z); + ImGui::SameLine(); + + const bool is_cut_plane_init = m_rotation_m.isApprox(Transform3d::Identity()) && bounding_box().center() == m_plane_center; + m_imgui->disabled_begin(is_cut_plane_init); + if (render_reset_button("cut_plane", _u8L("Reset cutting plane"))) + reset_cut_plane(); + m_imgui->disabled_end(); + + if (wxGetApp().plater()->printer_technology() == ptFFF) { + m_imgui->disabled_begin(!m_keep_upper || !m_keep_lower); + if (m_imgui->button(_L("Add/Edit connectors"))) + set_connectors_editing(true); + m_imgui->disabled_end(); + } + + ImGui::Separator(); + + auto render_part_action_line = [this, connectors](const wxString& label, const wxString& suffix, bool& keep_part, bool& place_on_cut_part, bool& rotate_part) { + bool keep = true; + ImGui::AlignTextToFramePadding(); + m_imgui->text(label); + + ImGui::SameLine(m_label_width); + + m_imgui->disabled_begin(!connectors.empty()); + m_imgui->checkbox(_L("Keep") + suffix, connectors.empty() ? keep_part : keep); + m_imgui->disabled_end(); + + ImGui::SameLine(2 * m_label_width); + + m_imgui->disabled_begin(!keep_part); + if (m_imgui->checkbox(_L("Place on cut") + suffix, place_on_cut_part)) + rotate_part = false; + ImGui::SameLine(); + if (m_imgui->checkbox(_L("Flip") + suffix, rotate_part)) + place_on_cut_part = false; + m_imgui->disabled_end(); + }; + + m_imgui->text(_L("After cut") + ": "); + render_part_action_line( _L("Upper part"), "##upper", m_keep_upper, m_place_on_cut_upper, m_rotate_upper); + render_part_action_line( _L("Lower part"), "##lower", m_keep_lower, m_place_on_cut_lower, m_rotate_lower); + } + + ImGui::Separator(); + + m_imgui->disabled_begin(!can_perform_cut()); + if(m_imgui->button(_L("Perform cut"))) + perform_cut(m_parent.get_selection()); + m_imgui->disabled_end(); +} + +void GLGizmoCut3D::validate_connector_settings() +{ + if (m_connector_depth_ratio < 0.f) + m_connector_depth_ratio = 3.f; + if (m_connector_depth_ratio_tolerance < 0.f) + m_connector_depth_ratio_tolerance = 0.1f; + if (m_connector_size < 0.f) + m_connector_size = 2.5f; + if (m_connector_size_tolerance < 0.f) + m_connector_size_tolerance = 0.f; + + if (m_connector_type == CutConnectorType::Undef) + m_connector_type = CutConnectorType::Plug; + if (m_connector_style == size_t(CutConnectorStyle::Undef)) + m_connector_style = size_t(CutConnectorStyle::Prizm); + if (m_connector_shape_id == size_t(CutConnectorShape::Undef)) + m_connector_shape_id = size_t(CutConnectorShape::Circle); +} + +void GLGizmoCut3D::init_input_window_data(CutConnectors &connectors) +{ + m_imperial_units = wxGetApp().app_config->get("use_inches") == "1"; + m_label_width = m_imgui->get_font_size() * 6.f; + m_control_width = m_imgui->get_font_size() * 9.f; + + if (m_connectors_editing && m_selected_count > 0) { + float depth_ratio { UndefFloat }; + float depth_ratio_tolerance { UndefFloat }; + float radius { UndefFloat }; + float radius_tolerance { UndefFloat }; + CutConnectorType type { CutConnectorType::Undef }; + CutConnectorStyle style { CutConnectorStyle::Undef }; + CutConnectorShape shape { CutConnectorShape::Undef }; + + bool is_init = false; + for (size_t idx = 0; idx < m_selected.size(); idx++) + if (m_selected[idx]) { + const CutConnector& connector = connectors[idx]; + if (!is_init) { + depth_ratio = connector.height; + depth_ratio_tolerance = connector.height_tolerance; + radius = connector.radius; + radius_tolerance = connector.radius_tolerance; + type = connector.attribs.type; + style = connector.attribs.style; + shape = connector.attribs.shape; + + if (m_selected_count == 1) + break; + is_init = true; + } + else { + if (!is_approx(depth_ratio, connector.height)) + depth_ratio = UndefFloat; + if (!is_approx(depth_ratio_tolerance, connector.height_tolerance)) + depth_ratio_tolerance = UndefFloat; + if (!is_approx(radius,connector.radius)) + radius = UndefFloat; + if (!is_approx(radius_tolerance, connector.radius_tolerance)) + radius_tolerance = UndefFloat; + + if (type != connector.attribs.type) + type = CutConnectorType::Undef; + if (style != connector.attribs.style) + style = CutConnectorStyle::Undef; + if (shape != connector.attribs.shape) + shape = CutConnectorShape::Undef; + } + } + + m_connector_depth_ratio = depth_ratio; + m_connector_depth_ratio_tolerance = depth_ratio_tolerance; + m_connector_size = 2.f * radius; + m_connector_size_tolerance = radius_tolerance; + m_connector_type = type; + m_connector_style = size_t(style); + m_connector_shape_id = size_t(shape); + } +} + +void GLGizmoCut3D::render_input_window_warning() const +{ + if (wxGetApp().plater()->printer_technology() == ptFFF && m_has_invalid_connector) { + wxString out = wxString(ImGui::WarningMarkerSmall) + _L("Invalid connectors detected") + ":"; + if (m_info_stats.outside_cut_contour > size_t(0)) + out += "\n - " + format_wxstr(_L_PLURAL("%1$d connector is out of cut contour", "%1$d connectors are out of cut contour", m_info_stats.outside_cut_contour), + m_info_stats.outside_cut_contour); + if (m_info_stats.outside_bb > size_t(0)) + out += "\n - " + format_wxstr(_L_PLURAL("%1$d connector is out of object", "%1$d connectors are out of object", m_info_stats.outside_bb), + m_info_stats.outside_bb); + if (m_info_stats.is_overlap) + out += "\n - " + _L("Some connectors are overlapped"); + m_imgui->text(out); + } + if (!m_keep_upper && !m_keep_lower) + m_imgui->text(wxString(ImGui::WarningMarkerSmall) + _L("Invalid state. \nNo one part is selected for keep after cut")); +} + +void GLGizmoCut3D::on_render_input_window(float x, float y, float bottom_limit) +{ + m_imgui->begin(get_name(), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + + // adjust window position to avoid overlap the view toolbar + adjust_window_position(x, y, bottom_limit); + + CutConnectors& connectors = m_c->selection_info()->model_object()->cut_connectors; + + init_input_window_data(connectors); + + if (m_connectors_editing) // connectors mode + render_connectors_input_window(connectors); + else + render_cut_plane_input_window(connectors); + + render_input_window_warning(); + + m_imgui->end(); + + render_debug_input_window(); +} + +// get volume transformation regarding to the "border". Border is related from the size of connectors +Transform3d GLGizmoCut3D::get_volume_transformation(const ModelVolume* volume) const +{ + bool is_prizm_dowel = m_connector_type == CutConnectorType::Dowel && m_connector_style == size_t(CutConnectorStyle::Prizm); + const Transform3d connector_trafo = assemble_transform( + is_prizm_dowel ? Vec3d(0.0, 0.0, -m_connector_depth_ratio) : Vec3d::Zero(), + Transformation(m_rotation_m).get_rotation(), + Vec3d(0.5*m_connector_size, 0.5*m_connector_size, is_prizm_dowel ? 2 * m_connector_depth_ratio : m_connector_depth_ratio), + Vec3d::Ones()); + const Vec3d connector_bb = m_connector_mesh.transformed_bounding_box(connector_trafo).size(); + + const Vec3d bb = volume->mesh().bounding_box().size(); + + // calculate an unused border - part of the the volume, where we can't put connectors + const Vec3d border_scale(connector_bb.x() / bb.x(), connector_bb.y() / bb.y(), connector_bb.z() / bb.z()); + + const Transform3d vol_matrix = volume->get_matrix(); + const Vec3d vol_trans = vol_matrix.translation(); + // offset of the volume will be changed after scaling, so calculate the needed offset and set it to a volume_trafo + const Vec3d offset(vol_trans.x() * border_scale.x(), vol_trans.y() * border_scale.y(), vol_trans.z() * border_scale.z()); + + // scale and translate volume to suppress to put connectors too close to the border + return assemble_transform(offset, Vec3d::Zero(), Vec3d::Ones() - border_scale, Vec3d::Ones()) * vol_matrix; +} + +bool GLGizmoCut3D::is_conflict_for_connector(size_t idx, const CutConnectors& connectors, const Vec3d cur_pos) +{ + // check if connector pos is out of clipping plane + if (m_c->object_clipper() && !m_c->object_clipper()->is_projection_inside_cut(cur_pos)) { + m_info_stats.outside_cut_contour++; + return true; + } + + const CutConnector& cur_connector = connectors[idx]; + const Transform3d matrix = translation_transform(cur_pos) * m_rotation_m * + scale_transform(Vec3f(cur_connector.radius, cur_connector.radius, cur_connector.height).cast()); + const BoundingBoxf3 cur_tbb = m_shapes[cur_connector.attribs].model.get_bounding_box().transformed(matrix); + + if (!bounding_box().contains(cur_tbb)) { + m_info_stats.outside_bb++; + return true; + } + + for (size_t i = 0; i < connectors.size(); ++i) { + if (i == idx) + continue; + const CutConnector& connector = connectors[i]; + + if ((connector.pos - cur_connector.pos).norm() < double(connector.radius + cur_connector.radius)) { + m_info_stats.is_overlap = true; + return true; + } + } + + return false; +} + +void GLGizmoCut3D::render_connectors() +{ + ::glEnable(GL_DEPTH_TEST); + + if (cut_line_processing() || m_connector_mode == CutConnectorMode::Auto || !m_c->selection_info()) + return; + + const ModelObject* mo = m_c->selection_info()->model_object(); + auto inst_id = m_c->selection_info()->get_active_instance(); + if (inst_id < 0) + return; + const CutConnectors& connectors = mo->cut_connectors; + if (connectors.size() != m_selected.size()) { + // #ysFIXME + clear_selection(); + m_selected.resize(connectors.size(), false); + } + + ColorRGBA render_color = CONNECTOR_DEF_COLOR; + + const ModelInstance* mi = mo->instances[inst_id]; + const Vec3d& instance_offset = mi->get_offset(); + const double sla_shift = double(m_c->selection_info()->get_sla_shift()); + + const ClippingPlane* cp = m_c->object_clipper()->get_clipping_plane(); + const Vec3d& normal = cp && cp->is_active() ? cp->get_normal() : m_clp_normal; + + m_has_invalid_connector = false; + m_info_stats.invalidate(); + + for (size_t i = 0; i < connectors.size(); ++i) { + const CutConnector& connector = connectors[i]; + + float height = connector.height; + // recalculate connector position to world position + Vec3d pos = connector.pos + instance_offset + sla_shift * Vec3d::UnitZ(); + + // First decide about the color of the point. + if (is_conflict_for_connector(i, connectors, pos)) { + m_has_invalid_connector = true; + render_color = CONNECTOR_ERR_COLOR; + } + else if (!m_connectors_editing) + render_color = CONNECTOR_ERR_COLOR; + else if (size_t(m_hover_id - m_connectors_group_id) == i) + render_color = connector.attribs.type == CutConnectorType::Dowel ? HOVERED_DOWEL_COLOR : HOVERED_PLAG_COLOR; + else if (m_selected[i]) + render_color = connector.attribs.type == CutConnectorType::Dowel ? SELECTED_DOWEL_COLOR : SELECTED_PLAG_COLOR; + else // neither hover nor picking + render_color = connector.attribs.type == CutConnectorType::Dowel ? DOWEL_COLOR : PLAG_COLOR; + + const Camera& camera = wxGetApp().plater()->get_camera(); + if (connector.attribs.type == CutConnectorType::Dowel && + connector.attribs.style == CutConnectorStyle::Prizm) { + pos -= height * normal; + height *= 2; + } + const Transform3d view_model_matrix = camera.get_view_matrix() * translation_transform(pos) * m_rotation_m * + scale_transform(Vec3f(connector.radius, connector.radius, height).cast()); + + render_model(m_shapes[connector.attribs].model, render_color, view_model_matrix); + } +} + +bool GLGizmoCut3D::can_perform_cut() const +{ + if (m_has_invalid_connector || (!m_keep_upper && !m_keep_lower) || m_connectors_editing) + return false; + + const auto clipper = m_c->object_clipper(); + return clipper && clipper->has_valid_contour(); +} + +void GLGizmoCut3D::apply_connectors_in_model(ModelObject* mo, bool &create_dowels_as_separate_object) +{ + if (m_connector_mode == CutConnectorMode::Manual) { + clear_selection(); + + for (CutConnector&connector : mo->cut_connectors) { + connector.rotation_m = m_rotation_m; + + if (connector.attribs.type == CutConnectorType::Dowel) { + if (connector.attribs.style == CutConnectorStyle::Prizm) + connector.height *= 2; + create_dowels_as_separate_object = true; + } + else { + // culculate shift of the connector center regarding to the position on the cut plane + Vec3d shifted_center = m_plane_center + Vec3d::UnitZ(); + rotate_vec3d_around_plane_center(shifted_center); + Vec3d norm = (shifted_center - m_plane_center).normalized(); + connector.pos += norm * 0.5 * double(connector.height); } } - else if (box.center() != m_cut_contours.position) { - m_cut_contours.shift = box.center() - m_cut_contours.position; + mo->apply_cut_connectors(_u8L("Connector")); + } +} + +void GLGizmoCut3D::perform_cut(const Selection& selection) +{ + if (!can_perform_cut()) + return; + const int instance_idx = selection.get_instance_idx(); + const int object_idx = selection.get_object_idx(); + + wxCHECK_RET(instance_idx >= 0 && object_idx >= 0, "GLGizmoCut: Invalid object selection"); + + Plater* plater = wxGetApp().plater(); + ModelObject* mo = plater->model().objects[object_idx]; + if (!mo) + return; + + // deactivate CutGizmo and than perform a cut + m_parent.reset_all_gizmos(); + + // m_cut_z is the distance from the bed. Subtract possible SLA elevation. + const double sla_shift_z = selection.get_first_volume()->get_sla_shift_z(); + + const Vec3d instance_offset = mo->instances[instance_idx]->get_offset(); + Vec3d cut_center_offset = m_plane_center - instance_offset; + cut_center_offset[Z] -= sla_shift_z; + + // perform cut + { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Cut by Plane")); + + bool create_dowels_as_separate_object = false; + const bool has_connectors = !mo->cut_connectors.empty(); + // update connectors pos as offset of its center before cut performing + apply_connectors_in_model(mo, create_dowels_as_separate_object); + + plater->cut(object_idx, instance_idx, assemble_transform(cut_center_offset) * m_rotation_m, + only_if(has_connectors ? true : m_keep_upper, ModelObjectCutAttribute::KeepUpper) | + only_if(has_connectors ? true : m_keep_lower, ModelObjectCutAttribute::KeepLower) | + only_if(m_place_on_cut_upper, ModelObjectCutAttribute::PlaceOnCutUpper) | + only_if(m_place_on_cut_lower, ModelObjectCutAttribute::PlaceOnCutLower) | + only_if(m_rotate_upper, ModelObjectCutAttribute::FlipUpper) | + only_if(m_rotate_lower, ModelObjectCutAttribute::FlipLower) | + only_if(create_dowels_as_separate_object, ModelObjectCutAttribute::CreateDowels)); + } +} + + + +// Unprojects the mouse position on the mesh and saves hit point and normal of the facet into pos_and_normal +// Return false if no intersection was found, true otherwise. +bool GLGizmoCut3D::unproject_on_cut_plane(const Vec2d& mouse_position, std::pair& pos_and_normal, Vec3d& pos_world) +{ + const float sla_shift = m_c->selection_info()->get_sla_shift(); + + const ModelObject* mo = m_c->selection_info()->model_object(); + const ModelInstance* mi = mo->instances[m_c->selection_info()->get_active_instance()]; + const Transform3d instance_trafo = sla_shift > 0.f ? + assemble_transform(Vec3d(0.0, 0.0, sla_shift)) * mi->get_transformation().get_matrix() : mi->get_transformation().get_matrix(); + const Camera& camera = wxGetApp().plater()->get_camera(); + + int mesh_id = -1; + for (const ModelVolume* mv : mo->volumes) { + ++mesh_id; + if (!mv->is_model_part()) + continue; + Vec3f normal; + Vec3f hit; + bool clipping_plane_was_hit = false; + +// const Transform3d volume_trafo = get_volume_transformation(mv); + const Transform3d volume_trafo = mv->get_transformation().get_matrix(); + + m_c->raycaster()->raycasters()[mesh_id]->unproject_on_mesh(mouse_position, instance_trafo * volume_trafo, + camera, hit, normal, m_c->object_clipper()->get_clipping_plane(true), + nullptr, &clipping_plane_was_hit); + if (clipping_plane_was_hit) { + // recalculate hit to object's local position + Vec3d hit_d = hit.cast(); + hit_d -= mi->get_offset(); + hit_d[Z] -= sla_shift; + + // Return both the point and the facet normal. + pos_and_normal = std::make_pair(hit_d, normal.cast()); + pos_world = hit.cast(); + return true; } } + return false; +} + +void GLGizmoCut3D::clear_selection() +{ + m_selected.clear(); + m_selected_count = 0; +} + +void GLGizmoCut3D::reset_connectors() +{ + m_c->selection_info()->model_object()->cut_connectors.clear(); + update_raycasters_for_picking(); + clear_selection(); +} + +void GLGizmoCut3D::init_connector_shapes() +{ + for (const CutConnectorType& type : {CutConnectorType::Dowel, CutConnectorType::Plug}) + for (const CutConnectorStyle& style : {CutConnectorStyle::Frustum, CutConnectorStyle::Prizm}) + for (const CutConnectorShape& shape : {CutConnectorShape::Circle, CutConnectorShape::Hexagon, CutConnectorShape::Square, CutConnectorShape::Triangle}) { + const CutConnectorAttributes attribs = { type, style, shape }; + const indexed_triangle_set its = ModelObject::get_connector_mesh(attribs); + m_shapes[attribs].model.init_from(its); + m_shapes[attribs].mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); + } +} + +void GLGizmoCut3D::update_connector_shape() +{ + CutConnectorAttributes attribs = { m_connector_type, CutConnectorStyle(m_connector_style), CutConnectorShape(m_connector_shape_id) }; + + const indexed_triangle_set its = ModelObject::get_connector_mesh(attribs); + m_connector_mesh.clear(); + m_connector_mesh = TriangleMesh(its); +} + +bool GLGizmoCut3D::cut_line_processing() const +{ + return m_line_beg != Vec3d::Zero(); +} + +void GLGizmoCut3D::discard_cut_line_processing() +{ + m_line_beg = m_line_end = Vec3d::Zero(); +} + +bool GLGizmoCut3D::process_cut_line(SLAGizmoEventType action, const Vec2d& mouse_position) +{ + const Camera& camera = wxGetApp().plater()->get_camera(); + + Vec3d pt; + Vec3d dir; + MeshRaycaster::line_from_mouse_pos(mouse_position, Transform3d::Identity(), camera, pt, dir); + dir.normalize(); + pt += dir; // Move the pt along dir so it is not clipped. + + if (action == SLAGizmoEventType::LeftDown && !cut_line_processing()) { + m_line_beg = pt; + m_line_end = pt; + on_unregister_raycasters_for_picking(); + return true; + } + + if (cut_line_processing()) { + m_line_end = pt; + if (action == SLAGizmoEventType::LeftDown || action == SLAGizmoEventType::LeftUp) { + Vec3d line_dir = m_line_end - m_line_beg; + if (line_dir.norm() < 3.0) + return true; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Cut by line"), UndoRedo::SnapshotType::GizmoAction); + + Vec3d cross_dir = line_dir.cross(dir).normalized(); + Eigen::Quaterniond q; + Transform3d m = Transform3d::Identity(); + m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(Vec3d::UnitZ(), cross_dir).toRotationMatrix(); + + m_rotation_m = m; + m_angle_arc.reset(); + + set_center(m_plane_center + cross_dir * (cross_dir.dot(pt - m_plane_center))); + + discard_cut_line_processing(); + } + else if (action == SLAGizmoEventType::Moving) + this->set_dirty(); + return true; + } + return false; +} + +bool GLGizmoCut3D::add_connector(CutConnectors& connectors, const Vec2d& mouse_position) +{ + if (!m_connectors_editing) + return false; + + std::pair pos_and_normal; + Vec3d pos_world; + if (unproject_on_cut_plane(mouse_position.cast(), pos_and_normal, pos_world)) { + const Vec3d& hit = pos_and_normal.first; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Add connector"), UndoRedo::SnapshotType::GizmoAction); + unselect_all_connectors(); + + connectors.emplace_back(hit, m_rotation_m, + m_connector_size * 0.5f, m_connector_depth_ratio, + m_connector_size_tolerance, m_connector_depth_ratio_tolerance, + CutConnectorAttributes( CutConnectorType(m_connector_type), + CutConnectorStyle(m_connector_style), + CutConnectorShape(m_connector_shape_id))); + m_selected.push_back(true); + m_selected_count = 1; + assert(m_selected.size() == connectors.size()); + update_raycasters_for_picking(); + m_parent.set_as_dirty(); + + return true; + } + return false; +} + +bool GLGizmoCut3D::delete_selected_connectors(CutConnectors& connectors) +{ + if (connectors.empty()) + return false; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Delete connector"), UndoRedo::SnapshotType::GizmoAction); + + // remove connectors + for (int i = int(connectors.size()) - 1; i >= 0; i--) + if (m_selected[i]) + connectors.erase(connectors.begin() + i); + // remove selections + m_selected.erase(std::remove_if(m_selected.begin(), m_selected.end(), [](const auto& selected) { + return selected; }), m_selected.end()); + m_selected_count = 0; + + assert(m_selected.size() == connectors.size()); + update_raycasters_for_picking(); + m_parent.set_as_dirty(); + return true; +} + +void GLGizmoCut3D::select_connector(int idx, bool select) +{ + m_selected[idx] = select; + if (select) + ++m_selected_count; else - m_cut_contours.contours.reset(); + --m_selected_count; +} + +bool GLGizmoCut3D::is_selection_changed(bool alt_down, bool shift_down) +{ + if (m_hover_id >= m_connectors_group_id) { + if (alt_down) + select_connector(m_hover_id - m_connectors_group_id, false); + else { + if (!shift_down) + unselect_all_connectors(); + select_connector(m_hover_id - m_connectors_group_id, true); + } + return true; + } + return false; +} + +void GLGizmoCut3D::process_selection_rectangle(CutConnectors &connectors) +{ + GLSelectionRectangle::EState rectangle_status = m_selection_rectangle.get_state(); + + ModelObject* mo = m_c->selection_info()->model_object(); + int active_inst = m_c->selection_info()->get_active_instance(); + + // First collect positions of all the points in world coordinates. + Transformation trafo = mo->instances[active_inst]->get_transformation(); + trafo.set_offset(trafo.get_offset() + double(m_c->selection_info()->get_sla_shift()) * Vec3d::UnitZ()); + + std::vector points; + for (const CutConnector&connector : connectors) + points.push_back(connector.pos + trafo.get_offset()); + + // Now ask the rectangle which of the points are inside. + std::vector points_idxs = m_selection_rectangle.contains(points); + m_selection_rectangle.stop_dragging(); + + for (size_t idx : points_idxs) + select_connector(int(idx), rectangle_status == GLSelectionRectangle::EState::Select); +} + +bool GLGizmoCut3D::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) +{ + if (is_dragging() || m_connector_mode == CutConnectorMode::Auto || (!m_keep_upper || !m_keep_lower)) + return false; + + if ( m_hover_id < 0 && shift_down && ! m_connectors_editing && + (action == SLAGizmoEventType::LeftDown || action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::Moving) ) + return process_cut_line(action, mouse_position); + + if (!m_connectors_editing) + return false; + + CutConnectors& connectors = m_c->selection_info()->model_object()->cut_connectors; + + if (action == SLAGizmoEventType::LeftDown) { + if (shift_down || alt_down) { + // left down with shift - show the selection rectangle: + if (m_hover_id == -1) + m_selection_rectangle.start_dragging(mouse_position, shift_down ? GLSelectionRectangle::EState::Select : GLSelectionRectangle::EState::Deselect); + } + else + // If there is no selection and no hovering, add new point + if (m_hover_id == -1 && !shift_down && !alt_down) + if (!add_connector(connectors, mouse_position)) + m_ldown_mouse_position = mouse_position; + return true; + } + + if (action == SLAGizmoEventType::LeftUp && !m_selection_rectangle.is_dragging()) { + if ((m_ldown_mouse_position - mouse_position).norm() < 5.) + unselect_all_connectors(); + return is_selection_changed(alt_down, shift_down); + } + + // left up with selection rectangle - select points inside the rectangle: + if ((action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::ShiftUp || action == SLAGizmoEventType::AltUp) && m_selection_rectangle.is_dragging()) { + // Is this a selection or deselection rectangle? + process_selection_rectangle(connectors); + return true; + } + + // dragging the selection rectangle: + if (action == SLAGizmoEventType::Dragging) { + if (m_selection_rectangle.is_dragging()) { + m_selection_rectangle.dragging(mouse_position); + return true; + } + return false; + } + + if (action == SLAGizmoEventType::RightDown && !shift_down) { + // If any point is in hover state, this should initiate its move - return control back to GLCanvas: + if (m_hover_id < m_connectors_group_id) + return false; + unselect_all_connectors(); + select_connector(m_hover_id - m_connectors_group_id, true); + return delete_selected_connectors(connectors); + } + + if (action == SLAGizmoEventType::Delete) + return delete_selected_connectors(connectors); + + if (action == SLAGizmoEventType::SelectAll) { + select_all_connectors(); + return true; + } + + return false; +} + +CommonGizmosDataID GLGizmoCut3D::on_get_requirements() const { + return CommonGizmosDataID( + int(CommonGizmosDataID::SelectionInfo) + | int(CommonGizmosDataID::InstancesHider) + | int(CommonGizmosDataID::ObjectClipper) + | int(CommonGizmosDataID::Raycaster)); } } // namespace GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp index 0a7274e2e..35b6a92ce 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp @@ -2,55 +2,161 @@ #define slic3r_GLGizmoCut_hpp_ #include "GLGizmoBase.hpp" +#include "slic3r/GUI/GLSelectionRectangle.hpp" #include "slic3r/GUI/GLModel.hpp" #include "libslic3r/TriangleMesh.hpp" -#include "libslic3r/ObjectID.hpp" +#include "libslic3r/Model.hpp" namespace Slic3r { + +enum class CutConnectorType : int; +class ModelVolume; +struct CutConnectorAttributes; + namespace GUI { class Selection; -class GLGizmoCut : public GLGizmoBase -{ - static const double Offset; - static const double Margin; +enum class SLAGizmoEventType : unsigned char; + +class GLGizmoCut3D : public GLGizmoBase +{ + Transform3d m_rotation_m{ Transform3d::Identity() }; + double m_snap_step{ 1.0 }; + int m_connectors_group_id; + + // archived values + Vec3d m_ar_plane_center { Vec3d::Zero() }; + + Vec3d m_plane_center{ Vec3d::Zero() }; + // data to check position of the cut palne center on gizmo activation + Vec3d m_min_pos{ Vec3d::Zero() }; + Vec3d m_max_pos{ Vec3d::Zero() }; + Vec3d m_bb_center{ Vec3d::Zero() }; + Vec3d m_center_offset{ Vec3d::Zero() }; + + // values from RotationGizmo + double m_radius{ 0.0 }; + double m_grabber_radius{ 0.0 }; + double m_grabber_connection_len{ 0.0 }; + + double m_snap_coarse_in_radius{ 0.0 }; + double m_snap_coarse_out_radius{ 0.0 }; + double m_snap_fine_in_radius{ 0.0 }; + double m_snap_fine_out_radius{ 0.0 }; + + // dragging angel in hovered axes + Transform3d m_start_dragging_m{ Transform3d::Identity() }; + double m_angle{ 0.0 }; + + TriangleMesh m_connector_mesh; + // workaround for using of the clipping plane normal + Vec3d m_clp_normal{ Vec3d::Ones() }; + + Vec3d m_line_beg{ Vec3d::Zero() }; + Vec3d m_line_end{ Vec3d::Zero() }; + + Vec2d m_ldown_mouse_position{ Vec2d::Zero() }; - double m_cut_z{ 0.0 }; - double m_max_z{ 0.0 }; - double m_start_z{ 0.0 }; - Vec3d m_drag_pos; - Vec3d m_drag_center; - bool m_keep_upper{ true }; - bool m_keep_lower{ true }; - bool m_rotate_lower{ false }; -#if ENABLE_LEGACY_OPENGL_REMOVAL GLModel m_plane; GLModel m_grabber_connection; - Vec3d m_old_center; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL + GLModel m_cut_line; - struct CutContours + PickingModel m_sphere; + PickingModel m_cone; + std::map m_shapes; + std::vector> m_raycasters; + + GLModel m_circle; + GLModel m_scale; + GLModel m_snap_radii; + GLModel m_reference_radius; + GLModel m_angle_arc; + + Vec3d m_old_center; + + struct InvalidConnectorsStatistics { - TriangleMesh mesh; - GLModel contours; - double cut_z{ 0.0 }; - Vec3d position{ Vec3d::Zero() }; - Vec3d shift{ Vec3d::Zero() }; - ObjectID object_id; - int instance_idx{ -1 }; - std::vector volumes_idxs; - std::vector volumes_trafos; + unsigned int outside_cut_contour; + unsigned int outside_bb; + bool is_overlap; + + void invalidate() { + outside_cut_contour = 0; + outside_bb = 0; + is_overlap = false; + } + } m_info_stats; + + bool m_keep_upper{ true }; + bool m_keep_lower{ true }; + bool m_place_on_cut_upper{ true }; + bool m_place_on_cut_lower{ false }; + bool m_rotate_upper{ false }; + bool m_rotate_lower{ false }; + + bool m_hide_cut_plane{ false }; + bool m_connectors_editing{ false }; + bool m_cut_plane_as_circle{ false }; + + float m_connector_depth_ratio{ 3.f }; + float m_connector_size{ 2.5f }; + + float m_connector_depth_ratio_tolerance{ 0.1f }; + float m_connector_size_tolerance{ 0.f }; + + float m_label_width{ 150.0 }; + float m_control_width{ 200.0 }; + bool m_imperial_units{ false }; + bool force_update_clipper_on_render{false}; + + mutable std::vector m_selected; // which pins are currently selected + int m_selected_count{ 0 }; + + GLSelectionRectangle m_selection_rectangle; + + bool m_has_invalid_connector{ false }; + + bool m_show_shortcuts{ false }; + std::vector> m_shortcuts; + + enum class CutMode { + cutPlanar + , cutGrig + //,cutRadial + //,cutModular }; - CutContours m_cut_contours; + enum class CutConnectorMode { + Auto + , Manual + }; + + std::vector m_modes; + size_t m_mode{ size_t(CutMode::cutPlanar) }; + + std::vector m_connector_modes; + CutConnectorMode m_connector_mode{ CutConnectorMode::Manual }; + + std::vector m_connector_types; + CutConnectorType m_connector_type; + + std::vector m_connector_styles; + size_t m_connector_style; + + std::vector m_connector_shapes; + size_t m_connector_shape_id; + + std::vector m_axis_names; public: - GLGizmoCut(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); - - double get_cut_z() const { return m_cut_z; } - void set_cut_z(double cut_z); + GLGizmoCut3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); std::string get_tooltip() const override; + bool unproject_on_cut_plane(const Vec2d& mouse_pos, std::pair& pos_and_normal, Vec3d& pos_world); + bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down); + + bool is_in_editing_mode() const override { return m_connectors_editing; } + bool is_selection_rectangle_dragging() const override { return m_selection_rectangle.is_dragging(); } /// /// Drag of plane @@ -59,29 +165,105 @@ public: /// Return True when use the information otherwise False. bool on_mouse(const wxMouseEvent &mouse_event) override; + void shift_cut_z(double delta); + void rotate_vec3d_around_plane_center(Vec3d&vec); + void put_connectors_on_cut_plane(const Vec3d& cp_normal, double cp_offset); + void update_clipper(); + void update_clipper_on_render(); + void set_connectors_editing() { m_connectors_editing = true; } + void invalidate_cut_plane(); + + BoundingBoxf3 bounding_box() const; + BoundingBoxf3 transformed_bounding_box(const Vec3d& plane_center, bool revert_move = false) const; + protected: - virtual bool on_init() override; - virtual void on_load(cereal::BinaryInputArchive& ar) override { ar(m_cut_z, m_keep_upper, m_keep_lower, m_rotate_lower); } - virtual void on_save(cereal::BinaryOutputArchive& ar) const override { ar(m_cut_z, m_keep_upper, m_keep_lower, m_rotate_lower); } - virtual std::string on_get_name() const override; - virtual void on_set_state() override; - virtual bool on_is_activable() const override; - virtual void on_start_dragging() override; - virtual void on_dragging(const UpdateData& data) override; - virtual void on_render() override; -#if ENABLE_RAYCAST_PICKING + bool on_init() override; + void on_load(cereal::BinaryInputArchive&ar) override; + void on_save(cereal::BinaryOutputArchive&ar) const override; + std::string on_get_name() const override; + void on_set_state() override; + CommonGizmosDataID on_get_requirements() const override; + void on_set_hover_id() override; + bool on_is_activable() const override; + bool on_is_selectable() const override; + Vec3d mouse_position_in_local_plane(Axis axis, const Linef3&mouse_ray) const; + void dragging_grabber_z(const GLGizmoBase::UpdateData &data); + void dragging_grabber_xy(const GLGizmoBase::UpdateData &data); + void dragging_connector(const GLGizmoBase::UpdateData &data); + void on_dragging(const UpdateData&data) override; + void on_start_dragging() override; + void on_stop_dragging() override; + void on_render() override; + + void render_debug_input_window(); + void adjust_window_position(float x, float y, float bottom_limit); + void unselect_all_connectors(); + void select_all_connectors(); + void render_shortcuts(); + void apply_selected_connectors(std::function apply_fn); + void render_connectors_input_window(CutConnectors &connectors); + void render_build_size(); + void reset_cut_plane(); + void set_connectors_editing(bool connectors_editing); + void render_cut_plane_input_window(CutConnectors &connectors); + void init_input_window_data(CutConnectors &connectors); + void render_input_window_warning() const; + bool add_connector(CutConnectors&connectors, const Vec2d&mouse_position); + bool delete_selected_connectors(CutConnectors&connectors); + void select_connector(int idx, bool select); + bool is_selection_changed(bool alt_down, bool shift_down); + void process_selection_rectangle(CutConnectors &connectors); + virtual void on_register_raycasters_for_picking() override; virtual void on_unregister_raycasters_for_picking() override; -#else - virtual void on_render_for_picking() override; -#endif // ENABLE_RAYCAST_PICKING - virtual void on_render_input_window(float x, float y, float bottom_limit) override; + void update_raycasters_for_picking(); + void set_volumes_picking_state(bool state); + void update_raycasters_for_picking_transform(); + + void on_render_input_window(float x, float y, float bottom_limit) override; + + bool wants_enter_leave_snapshots() const override { return true; } + std::string get_gizmo_entering_text() const override { return _u8L("Entering Cut gizmo"); } + std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Cut gizmo"); } + std::string get_action_snapshot_name() override { return _u8L("Cut gizmo editing"); } private: - void perform_cut(const Selection& selection); - double calc_projection(const Linef3& mouse_ray) const; - BoundingBoxf3 bounding_box() const; - void update_contours(); + void set_center(const Vec3d& center); + bool render_combo(const std::string& label, const std::vector& lines, size_t& selection_idx); + bool render_double_input(const std::string& label, double& value_in); + bool render_slider_double_input(const std::string& label, float& value_in, float& tolerance_in); + void render_move_center_input(int axis); + void render_connect_mode_radio_button(CutConnectorMode mode); + bool render_reset_button(const std::string& label_id, const std::string& tooltip) const; + bool render_connect_type_radio_button(CutConnectorType type); + Transform3d get_volume_transformation(const ModelVolume* volume) const; + bool is_conflict_for_connector(size_t idx, const CutConnectors& connectors, const Vec3d cur_pos); + void render_connectors(); + + bool can_perform_cut() const; + void apply_connectors_in_model(ModelObject* mo, bool &create_dowels_as_separate_object); + bool cut_line_processing() const; + void discard_cut_line_processing(); + + void render_cut_plane(); + void render_model(GLModel& model, const ColorRGBA& color, Transform3d view_model_matrix); + void render_line(GLModel& line_model, const ColorRGBA& color, Transform3d view_model_matrix, float width); + void render_rotation_snapping(Axis axis, const ColorRGBA& color); + void render_grabber_connection(const ColorRGBA& color, Transform3d view_matrix); + void render_cut_plane_grabbers(); + void render_cut_line(); + void perform_cut(const Selection&selection); + void set_center_pos(const Vec3d¢er_pos, bool force = false); + bool update_bb(); + void init_picking_models(); + void init_rendering_items(); + void render_clipper_cut(); + void clear_selection(); + void reset_connectors(); + void init_connector_shapes(); + void update_connector_shape(); + void validate_connector_settings(); + bool process_cut_line(SLAGizmoEventType action, const Vec2d& mouse_position); }; } // namespace GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index b30c29db1..5f0a110ce 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -1,550 +1,550 @@ -#include "GLGizmoFdmSupports.hpp" - -#include "libslic3r/Model.hpp" - -//#include "slic3r/GUI/3DScene.hpp" -#include "slic3r/GUI/GLCanvas3D.hpp" -#include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/ImGuiWrapper.hpp" -#include "slic3r/GUI/Plater.hpp" -#include "slic3r/GUI/GUI_ObjectList.hpp" -#include "slic3r/GUI/format.hpp" -#include "slic3r/Utils/UndoRedo.hpp" -#include "libslic3r/Print.hpp" -#include "slic3r/GUI/MsgDialog.hpp" - - -#include - - -namespace Slic3r::GUI { - - - -void GLGizmoFdmSupports::on_shutdown() -{ - m_highlight_by_angle_threshold_deg = 0.f; - m_parent.use_slope(false); - m_parent.toggle_model_objects_visibility(true); -} - - - -std::string GLGizmoFdmSupports::on_get_name() const -{ - return _u8L("Paint-on supports"); -} - - - -bool GLGizmoFdmSupports::on_init() -{ - m_shortcut_key = WXK_CONTROL_L; - - m_desc["auto_generate"] = _L("Auto-generate supports"); - m_desc["generating"] = _L("Generating supports..."); - m_desc["clipping_of_view"] = _L("Clipping of view") + ": "; - m_desc["reset_direction"] = _L("Reset direction"); - m_desc["cursor_size"] = _L("Brush size") + ": "; - m_desc["cursor_type"] = _L("Brush shape") + ": "; - m_desc["enforce_caption"] = _L("Left mouse button") + ": "; - m_desc["enforce"] = _L("Enforce supports"); - m_desc["block_caption"] = _L("Right mouse button") + ": "; - m_desc["block"] = _L("Block supports"); - m_desc["remove_caption"] = _L("Shift + Left mouse button") + ": "; - m_desc["remove"] = _L("Remove selection"); - m_desc["remove_all"] = _L("Remove all selection"); - m_desc["circle"] = _L("Circle"); - m_desc["sphere"] = _L("Sphere"); - m_desc["pointer"] = _L("Triangles"); - m_desc["highlight_by_angle"] = _L("Highlight overhang by angle"); - m_desc["enforce_button"] = _L("Enforce"); - m_desc["cancel"] = _L("Cancel"); - - m_desc["tool_type"] = _L("Tool type") + ": "; - m_desc["tool_brush"] = _L("Brush"); - m_desc["tool_smart_fill"] = _L("Smart fill"); - - m_desc["smart_fill_angle"] = _L("Smart fill angle"); - - m_desc["split_triangles"] = _L("Split triangles"); - m_desc["on_overhangs_only"] = _L("On overhangs only"); - - return true; -} - -void GLGizmoFdmSupports::render_painter_gizmo() -{ - const Selection& selection = m_parent.get_selection(); - - glsafe(::glEnable(GL_BLEND)); - glsafe(::glEnable(GL_DEPTH_TEST)); - - render_triangles(selection); - m_c->object_clipper()->render_cut(); - m_c->instances_hider()->render_cut(); - render_cursor(); - - glsafe(::glDisable(GL_BLEND)); -} - - - -void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_limit) -{ - if (! m_c->selection_info()->model_object()) - return; - - const float approx_height = m_imgui->scaled(25.f); - y = std::min(y, bottom_limit - approx_height); - m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); - - m_imgui->begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); - - // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: - const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x, - m_imgui->calc_text_size(m_desc.at("reset_direction")).x) + m_imgui->scaled(1.5f); - const float cursor_slider_left = m_imgui->calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.f); - const float smart_fill_slider_left = m_imgui->calc_text_size(m_desc.at("smart_fill_angle")).x + m_imgui->scaled(1.f); - const float autoset_slider_label_max_width = m_imgui->scaled(7.5f); - const float autoset_slider_left = m_imgui->calc_text_size(m_desc.at("highlight_by_angle"), autoset_slider_label_max_width).x + m_imgui->scaled(1.f); - - const float cursor_type_radio_circle = m_imgui->calc_text_size(m_desc["circle"]).x + m_imgui->scaled(2.5f); - const float cursor_type_radio_sphere = m_imgui->calc_text_size(m_desc["sphere"]).x + m_imgui->scaled(2.5f); - const float cursor_type_radio_pointer = m_imgui->calc_text_size(m_desc["pointer"]).x + m_imgui->scaled(2.5f); - - const float button_width = m_imgui->calc_text_size(m_desc.at("remove_all")).x + m_imgui->scaled(1.f); - const float button_enforce_width = m_imgui->calc_text_size(m_desc.at("enforce_button")).x; - const float button_cancel_width = m_imgui->calc_text_size(m_desc.at("cancel")).x; - const float buttons_width = std::max(button_enforce_width, button_cancel_width) + m_imgui->scaled(0.5f); - const float minimal_slider_width = m_imgui->scaled(4.f); - - const float tool_type_radio_left = m_imgui->calc_text_size(m_desc["tool_type"]).x + m_imgui->scaled(1.f); - const float tool_type_radio_brush = m_imgui->calc_text_size(m_desc["tool_brush"]).x + m_imgui->scaled(2.5f); - const float tool_type_radio_smart_fill = m_imgui->calc_text_size(m_desc["tool_smart_fill"]).x + m_imgui->scaled(2.5f); - - const float split_triangles_checkbox_width = m_imgui->calc_text_size(m_desc["split_triangles"]).x + m_imgui->scaled(2.5f); - const float on_overhangs_only_checkbox_width = m_imgui->calc_text_size(m_desc["on_overhangs_only"]).x + m_imgui->scaled(2.5f); - - float caption_max = 0.f; - float total_text_max = 0.f; - for (const auto &t : std::array{"enforce", "block", "remove"}) { - caption_max = std::max(caption_max, m_imgui->calc_text_size(m_desc[t + "_caption"]).x); - total_text_max = std::max(total_text_max, m_imgui->calc_text_size(m_desc[t]).x); - } - total_text_max += caption_max + m_imgui->scaled(1.f); - caption_max += m_imgui->scaled(1.f); - - const float sliders_left_width = std::max(std::max(autoset_slider_left, smart_fill_slider_left), std::max(cursor_slider_left, clipping_slider_left)); - const float slider_icon_width = m_imgui->get_slider_icon_size().x; - float window_width = minimal_slider_width + sliders_left_width + slider_icon_width; - window_width = std::max(window_width, total_text_max); - window_width = std::max(window_width, button_width); - window_width = std::max(window_width, split_triangles_checkbox_width); - window_width = std::max(window_width, on_overhangs_only_checkbox_width); - window_width = std::max(window_width, cursor_type_radio_circle + cursor_type_radio_sphere + cursor_type_radio_pointer); - window_width = std::max(window_width, tool_type_radio_left + tool_type_radio_brush + tool_type_radio_smart_fill); - window_width = std::max(window_width, 2.f * buttons_width + m_imgui->scaled(1.f)); - - auto draw_text_with_caption = [this, &caption_max](const wxString& caption, const wxString& text) { - m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, caption); - ImGui::SameLine(caption_max); - m_imgui->text(text); - }; - - for (const auto &t : std::array{"enforce", "block", "remove"}) - draw_text_with_caption(m_desc.at(t + "_caption"), m_desc.at(t)); - - ImGui::Separator(); - - if (waiting_for_autogenerated_supports) { - m_imgui->text(m_desc.at("generating")); - } else { - bool generate = m_imgui->button(m_desc.at("auto_generate")); - if (generate) - auto_generate(); - } - ImGui::Separator(); - - float position_before_text_y = ImGui::GetCursorPos().y; - ImGui::AlignTextToFramePadding(); - m_imgui->text_wrapped(m_desc["highlight_by_angle"] + ":", autoset_slider_label_max_width); - ImGui::AlignTextToFramePadding(); - float position_after_text_y = ImGui::GetCursorPos().y; - std::string format_str = std::string("%.f") + I18N::translate_utf8("°", - "Degree sign to use in the respective slider in FDM supports gizmo," - "placed after the number with no whitespace in between."); - ImGui::SameLine(sliders_left_width); - - float slider_height = m_imgui->get_slider_float_height(); - // Makes slider to be aligned to bottom of the multi-line text. - float slider_start_position_y = std::max(position_before_text_y, position_after_text_y - slider_height); - ImGui::SetCursorPosY(slider_start_position_y); - - ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); - wxString tooltip = format_wxstr(_L("Preselects faces by overhang angle. It is possible to restrict paintable facets to only preselected faces when " - "the option \"%1%\" is enabled."), m_desc["on_overhangs_only"]); - if (m_imgui->slider_float("##angle_threshold_deg", &m_highlight_by_angle_threshold_deg, 0.f, 90.f, format_str.data(), 1.0f, true, tooltip)) { - m_parent.set_slope_normal_angle(90.f - m_highlight_by_angle_threshold_deg); - if (! m_parent.is_using_slope()) { - m_parent.use_slope(true); - m_parent.set_as_dirty(); - } - } - - // Restores the cursor position to be below the multi-line text. - ImGui::SetCursorPosY(std::max(position_before_text_y + slider_height, position_after_text_y)); - - const float max_tooltip_width = ImGui::GetFontSize() * 20.0f; - - m_imgui->disabled_begin(m_highlight_by_angle_threshold_deg == 0.f); - ImGui::NewLine(); - ImGui::SameLine(window_width - 2.f*buttons_width - m_imgui->scaled(0.5f)); - if (m_imgui->button(m_desc["enforce_button"], buttons_width, 0.f)) { - select_facets_by_angle(m_highlight_by_angle_threshold_deg, false); - m_highlight_by_angle_threshold_deg = 0.f; - m_parent.use_slope(false); - } - ImGui::SameLine(window_width - buttons_width); - if (m_imgui->button(m_desc["cancel"], buttons_width, 0.f)) { - m_highlight_by_angle_threshold_deg = 0.f; - m_parent.use_slope(false); - } - m_imgui->disabled_end(); - - - ImGui::Separator(); - - ImGui::AlignTextToFramePadding(); - m_imgui->text(m_desc["tool_type"]); - - float tool_type_offset = tool_type_radio_left + (window_width - tool_type_radio_left - tool_type_radio_brush - tool_type_radio_smart_fill + m_imgui->scaled(0.5f)) / 2.f; - ImGui::SameLine(tool_type_offset); - ImGui::PushItemWidth(tool_type_radio_brush); - if (m_imgui->radio_button(m_desc["tool_brush"], m_tool_type == ToolType::BRUSH)) - m_tool_type = ToolType::BRUSH; - - if (ImGui::IsItemHovered()) - m_imgui->tooltip(_L("Paints facets according to the chosen painting brush."), max_tooltip_width); - - ImGui::SameLine(tool_type_offset + tool_type_radio_brush); - ImGui::PushItemWidth(tool_type_radio_smart_fill); - if (m_imgui->radio_button(m_desc["tool_smart_fill"], m_tool_type == ToolType::SMART_FILL)) - m_tool_type = ToolType::SMART_FILL; - - if (ImGui::IsItemHovered()) - m_imgui->tooltip(_L("Paints neighboring facets whose relative angle is less or equal to set angle."), max_tooltip_width); - - m_imgui->checkbox(m_desc["on_overhangs_only"], m_paint_on_overhangs_only); - if (ImGui::IsItemHovered()) - m_imgui->tooltip(format_wxstr(_L("Allows painting only on facets selected by: \"%1%\""), m_desc["highlight_by_angle"]), max_tooltip_width); - - ImGui::Separator(); - - if (m_tool_type == ToolType::BRUSH) { - m_imgui->text(m_desc.at("cursor_type")); - ImGui::NewLine(); - - float cursor_type_offset = (window_width - cursor_type_radio_sphere - cursor_type_radio_circle - cursor_type_radio_pointer + m_imgui->scaled(1.5f)) / 2.f; - ImGui::SameLine(cursor_type_offset); - ImGui::PushItemWidth(cursor_type_radio_sphere); - if (m_imgui->radio_button(m_desc["sphere"], m_cursor_type == TriangleSelector::CursorType::SPHERE)) - m_cursor_type = TriangleSelector::CursorType::SPHERE; - - if (ImGui::IsItemHovered()) - m_imgui->tooltip(_L("Paints all facets inside, regardless of their orientation."), max_tooltip_width); - - ImGui::SameLine(cursor_type_offset + cursor_type_radio_sphere); - ImGui::PushItemWidth(cursor_type_radio_circle); - - if (m_imgui->radio_button(m_desc["circle"], m_cursor_type == TriangleSelector::CursorType::CIRCLE)) - m_cursor_type = TriangleSelector::CursorType::CIRCLE; - - if (ImGui::IsItemHovered()) - m_imgui->tooltip(_L("Ignores facets facing away from the camera."), max_tooltip_width); - - ImGui::SameLine(cursor_type_offset + cursor_type_radio_sphere + cursor_type_radio_circle); - ImGui::PushItemWidth(cursor_type_radio_pointer); - - if (m_imgui->radio_button(m_desc["pointer"], m_cursor_type == TriangleSelector::CursorType::POINTER)) - m_cursor_type = TriangleSelector::CursorType::POINTER; - - if (ImGui::IsItemHovered()) - m_imgui->tooltip(_L("Paints only one facet."), max_tooltip_width); - - m_imgui->disabled_begin(m_cursor_type != TriangleSelector::CursorType::SPHERE && m_cursor_type != TriangleSelector::CursorType::CIRCLE); - - ImGui::AlignTextToFramePadding(); - m_imgui->text(m_desc.at("cursor_size")); - ImGui::SameLine(sliders_left_width); - ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); - m_imgui->slider_float("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f", 1.0f, true, _L("Alt + Mouse wheel")); - - m_imgui->checkbox(m_desc["split_triangles"], m_triangle_splitting_enabled); - - if (ImGui::IsItemHovered()) - m_imgui->tooltip(_L("Splits bigger facets into smaller ones while the object is painted."), max_tooltip_width); - - m_imgui->disabled_end(); - } else { - assert(m_tool_type == ToolType::SMART_FILL); - ImGui::AlignTextToFramePadding(); - m_imgui->text(m_desc["smart_fill_angle"] + ":"); - - ImGui::SameLine(sliders_left_width); - ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); - if (m_imgui->slider_float("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str.data(), 1.0f, true, _L("Alt + Mouse wheel"))) - for (auto &triangle_selector : m_triangle_selectors) { - triangle_selector->seed_fill_unselect_all_triangles(); - triangle_selector->request_update_render_data(); - } - } - - ImGui::Separator(); - if (m_c->object_clipper()->get_position() == 0.f) { - ImGui::AlignTextToFramePadding(); - m_imgui->text(m_desc.at("clipping_of_view")); - } - else { - if (m_imgui->button(m_desc.at("reset_direction"))) { - wxGetApp().CallAfter([this](){ - m_c->object_clipper()->set_position(-1., false); - }); - } - } - - auto clp_dist = float(m_c->object_clipper()->get_position()); - ImGui::SameLine(sliders_left_width); - ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); - if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true, _L("Ctrl + Mouse wheel"))) - m_c->object_clipper()->set_position(clp_dist, true); - - ImGui::Separator(); - if (m_imgui->button(m_desc.at("remove_all"))) { - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Reset selection"), UndoRedo::SnapshotType::GizmoAction); - ModelObject *mo = m_c->selection_info()->model_object(); - int idx = -1; - for (ModelVolume *mv : mo->volumes) - if (mv->is_model_part()) { - ++idx; - m_triangle_selectors[idx]->reset(); - m_triangle_selectors[idx]->request_update_render_data(); - } - - update_model_object(); - this->waiting_for_autogenerated_supports = false; - m_parent.set_as_dirty(); - } - - m_imgui->end(); -} - - - -void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) -{ - float threshold = (float(M_PI)/180.f)*threshold_deg; - const Selection& selection = m_parent.get_selection(); - const ModelObject* mo = m_c->selection_info()->model_object(); - const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; - - int mesh_id = -1; - for (const ModelVolume* mv : mo->volumes) { - if (! mv->is_model_part()) - continue; - - ++mesh_id; - -#if ENABLE_WORLD_COORDINATE - const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset(); -#else - const Transform3d trafo_matrix = mi->get_matrix(true) * mv->get_matrix(true); -#endif // ENABLE_WORLD_COORDINATE - Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast().normalized(); - Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); - - float dot_limit = limit.dot(down); - - // Now calculate dot product of vert_direction and facets' normals. - int idx = 0; - const indexed_triangle_set &its = mv->mesh().its; - for (const stl_triangle_vertex_indices &face : its.indices) { - if (its_face_normal(its, face).dot(down) > dot_limit) { - m_triangle_selectors[mesh_id]->set_facet(idx, block ? EnforcerBlockerType::BLOCKER : EnforcerBlockerType::ENFORCER); - m_triangle_selectors.back()->request_update_render_data(); - } - ++ idx; - } - } - - Plater::TakeSnapshot snapshot(wxGetApp().plater(), block ? _L("Block supports by angle") - : _L("Add supports by angle")); - update_model_object(); - this->waiting_for_autogenerated_supports = false; - m_parent.set_as_dirty(); -} - -void GLGizmoFdmSupports::data_changed() -{ - GLGizmoPainterBase::data_changed(); - if (! m_c->selection_info()) - return; - - ModelObject* mo = m_c->selection_info()->model_object(); - if (mo && this->waiting_for_autogenerated_supports) { - get_data_from_backend(); - } else { - this->waiting_for_autogenerated_supports = false; - } -} - -void GLGizmoFdmSupports::get_data_from_backend() -{ - if (! has_backend_supports()) - return; - ModelObject* mo = m_c->selection_info()->model_object(); - - // find the respective PrintObject, we need a pointer to it - for (const PrintObject* po : m_parent.fff_print()->objects()) { - if (po->model_object()->id() == mo->id()) { - std::unordered_map mvs; - for (const ModelVolume* mv : po->model_object()->volumes) { - if (mv->is_model_part()) { - mvs.emplace(mv->id().id, mv); - } - } - int mesh_id = -1.0f; - for (ModelVolume* mv : mo->volumes){ - if (mv->is_model_part()){ - mesh_id++; - mv->supported_facets.assign(mvs[mv->id().id]->supported_facets); - m_triangle_selectors[mesh_id]->deserialize(mv->supported_facets.get_data(), true); - m_triangle_selectors[mesh_id]->request_update_render_data(); - } - } - this->waiting_for_autogenerated_supports = false; - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - m_parent.set_as_dirty(); - } - } -} - - -void GLGizmoFdmSupports::update_model_object() const -{ - bool updated = false; - ModelObject* mo = m_c->selection_info()->model_object(); - int idx = -1; - for (ModelVolume* mv : mo->volumes) { - if (! mv->is_model_part()) - continue; - ++idx; - updated |= mv->supported_facets.set(*m_triangle_selectors[idx].get()); - } - - if (updated) { - const ModelObjectPtrs& mos = wxGetApp().model().objects; - wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin()); - - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - } -} - -void GLGizmoFdmSupports::update_from_model_object() -{ - wxBusyCursor wait; - - const ModelObject* mo = m_c->selection_info()->model_object(); - m_triangle_selectors.clear(); - - int volume_id = -1; - for (const ModelVolume* mv : mo->volumes) { - if (! mv->is_model_part()) - continue; - - ++volume_id; - - // This mesh does not account for the possible Z up SLA offset. - const TriangleMesh* mesh = &mv->mesh(); - - m_triangle_selectors.emplace_back(std::make_unique(*mesh)); - // Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize(). - m_triangle_selectors.back()->deserialize(mv->supported_facets.get_data(), false); - m_triangle_selectors.back()->request_update_render_data(); - } -} - -bool GLGizmoFdmSupports::has_backend_supports() const -{ - const ModelObject* mo = m_c->selection_info()->model_object(); - if (! mo) - return false; - - // find PrintObject with this ID - for (const PrintObject* po : m_parent.fff_print()->objects()) { - if (po->model_object()->id() == mo->id()) - return po->is_step_done(posSupportSpotsSearch); - } - return false; -} - -void GLGizmoFdmSupports::reslice_FDM_supports(bool postpone_error_messages) const { - wxGetApp().CallAfter( - [this, postpone_error_messages]() { - wxGetApp().plater()->reslice_FFF_until_step(posSupportSpotsSearch, - *m_c->selection_info()->model_object(), postpone_error_messages); - }); -} - -void GLGizmoFdmSupports::auto_generate() -{ - ModelObject *mo = m_c->selection_info()->model_object(); - bool not_painted = std::all_of(mo->volumes.begin(), mo->volumes.end(), [](const ModelVolume* vol){ - return vol->type() != ModelVolumeType::MODEL_PART || vol->supported_facets.empty(); - }); - - MessageDialog dlg(GUI::wxGetApp().plater(), - _L("Autogeneration will erase all currently painted areas.") + "\n\n" + - _L("Are you sure you want to do it?") + "\n", - _L("Warning"), wxICON_WARNING | wxYES | wxNO); - - if (not_painted || dlg.ShowModal() == wxID_YES) { - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points")); - int mesh_id = -1.0f; - for (ModelVolume *mv : mo->volumes) { - if (mv->is_model_part()) { - mesh_id++; - mv->supported_facets.reset(); - m_triangle_selectors[mesh_id]->reset(); - m_triangle_selectors[mesh_id]->request_update_render_data(); - } - } - - mo->config.set("support_material", true); - mo->config.set("support_material_auto", false); - this->waiting_for_autogenerated_supports = true; - wxGetApp().CallAfter([this]() { reslice_FDM_supports(); }); - } -} - - -PainterGizmoType GLGizmoFdmSupports::get_painter_type() const -{ - return PainterGizmoType::FDM_SUPPORTS; -} - -wxString GLGizmoFdmSupports::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const -{ - wxString action_name; - if (shift_down) - action_name = _L("Remove selection"); - else { - if (button_down == Button::Left) - action_name = _L("Add supports"); - else - action_name = _L("Block supports"); - } - return action_name; -} - -} // namespace Slic3r::GUI +#include "GLGizmoFdmSupports.hpp" + +#include "libslic3r/Model.hpp" + +//#include "slic3r/GUI/3DScene.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/ImGuiWrapper.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/Utils/UndoRedo.hpp" +#include "libslic3r/Print.hpp" +#include "slic3r/GUI/MsgDialog.hpp" + + +#include + + +namespace Slic3r::GUI { + + + +void GLGizmoFdmSupports::on_shutdown() +{ + m_highlight_by_angle_threshold_deg = 0.f; + m_parent.use_slope(false); + m_parent.toggle_model_objects_visibility(true); +} + + + +std::string GLGizmoFdmSupports::on_get_name() const +{ + return _u8L("Paint-on supports"); +} + + + +bool GLGizmoFdmSupports::on_init() +{ + m_shortcut_key = WXK_CONTROL_L; + + m_desc["auto_generate"] = _L("Auto-generate supports"); + m_desc["generating"] = _L("Generating supports..."); + m_desc["clipping_of_view"] = _L("Clipping of view") + ": "; + m_desc["reset_direction"] = _L("Reset direction"); + m_desc["cursor_size"] = _L("Brush size") + ": "; + m_desc["cursor_type"] = _L("Brush shape") + ": "; + m_desc["enforce_caption"] = _L("Left mouse button") + ": "; + m_desc["enforce"] = _L("Enforce supports"); + m_desc["block_caption"] = _L("Right mouse button") + ": "; + m_desc["block"] = _L("Block supports"); + m_desc["remove_caption"] = _L("Shift + Left mouse button") + ": "; + m_desc["remove"] = _L("Remove selection"); + m_desc["remove_all"] = _L("Remove all selection"); + m_desc["circle"] = _L("Circle"); + m_desc["sphere"] = _L("Sphere"); + m_desc["pointer"] = _L("Triangles"); + m_desc["highlight_by_angle"] = _L("Highlight overhang by angle"); + m_desc["enforce_button"] = _L("Enforce"); + m_desc["cancel"] = _L("Cancel"); + + m_desc["tool_type"] = _L("Tool type") + ": "; + m_desc["tool_brush"] = _L("Brush"); + m_desc["tool_smart_fill"] = _L("Smart fill"); + + m_desc["smart_fill_angle"] = _L("Smart fill angle"); + + m_desc["split_triangles"] = _L("Split triangles"); + m_desc["on_overhangs_only"] = _L("On overhangs only"); + + return true; +} + +void GLGizmoFdmSupports::render_painter_gizmo() +{ + const Selection& selection = m_parent.get_selection(); + + glsafe(::glEnable(GL_BLEND)); + glsafe(::glEnable(GL_DEPTH_TEST)); + + render_triangles(selection); + m_c->object_clipper()->render_cut(); + m_c->instances_hider()->render_cut(); + render_cursor(); + + glsafe(::glDisable(GL_BLEND)); +} + + + +void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_limit) +{ + if (! m_c->selection_info()->model_object()) + return; + + const float approx_height = m_imgui->scaled(25.f); + y = std::min(y, bottom_limit - approx_height); + m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); + + m_imgui->begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); + + // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: + const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x, + m_imgui->calc_text_size(m_desc.at("reset_direction")).x) + m_imgui->scaled(1.5f); + const float cursor_slider_left = m_imgui->calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.f); + const float smart_fill_slider_left = m_imgui->calc_text_size(m_desc.at("smart_fill_angle")).x + m_imgui->scaled(1.f); + const float autoset_slider_label_max_width = m_imgui->scaled(7.5f); + const float autoset_slider_left = m_imgui->calc_text_size(m_desc.at("highlight_by_angle"), autoset_slider_label_max_width).x + m_imgui->scaled(1.f); + + const float cursor_type_radio_circle = m_imgui->calc_text_size(m_desc["circle"]).x + m_imgui->scaled(2.5f); + const float cursor_type_radio_sphere = m_imgui->calc_text_size(m_desc["sphere"]).x + m_imgui->scaled(2.5f); + const float cursor_type_radio_pointer = m_imgui->calc_text_size(m_desc["pointer"]).x + m_imgui->scaled(2.5f); + + const float button_width = m_imgui->calc_text_size(m_desc.at("remove_all")).x + m_imgui->scaled(1.f); + const float button_enforce_width = m_imgui->calc_text_size(m_desc.at("enforce_button")).x; + const float button_cancel_width = m_imgui->calc_text_size(m_desc.at("cancel")).x; + const float buttons_width = std::max(button_enforce_width, button_cancel_width) + m_imgui->scaled(0.5f); + const float minimal_slider_width = m_imgui->scaled(4.f); + + const float tool_type_radio_left = m_imgui->calc_text_size(m_desc["tool_type"]).x + m_imgui->scaled(1.f); + const float tool_type_radio_brush = m_imgui->calc_text_size(m_desc["tool_brush"]).x + m_imgui->scaled(2.5f); + const float tool_type_radio_smart_fill = m_imgui->calc_text_size(m_desc["tool_smart_fill"]).x + m_imgui->scaled(2.5f); + + const float split_triangles_checkbox_width = m_imgui->calc_text_size(m_desc["split_triangles"]).x + m_imgui->scaled(2.5f); + const float on_overhangs_only_checkbox_width = m_imgui->calc_text_size(m_desc["on_overhangs_only"]).x + m_imgui->scaled(2.5f); + + float caption_max = 0.f; + float total_text_max = 0.f; + for (const auto &t : std::array{"enforce", "block", "remove"}) { + caption_max = std::max(caption_max, m_imgui->calc_text_size(m_desc[t + "_caption"]).x); + total_text_max = std::max(total_text_max, m_imgui->calc_text_size(m_desc[t]).x); + } + total_text_max += caption_max + m_imgui->scaled(1.f); + caption_max += m_imgui->scaled(1.f); + + const float sliders_left_width = std::max(std::max(autoset_slider_left, smart_fill_slider_left), std::max(cursor_slider_left, clipping_slider_left)); + const float slider_icon_width = m_imgui->get_slider_icon_size().x; + float window_width = minimal_slider_width + sliders_left_width + slider_icon_width; + window_width = std::max(window_width, total_text_max); + window_width = std::max(window_width, button_width); + window_width = std::max(window_width, split_triangles_checkbox_width); + window_width = std::max(window_width, on_overhangs_only_checkbox_width); + window_width = std::max(window_width, cursor_type_radio_circle + cursor_type_radio_sphere + cursor_type_radio_pointer); + window_width = std::max(window_width, tool_type_radio_left + tool_type_radio_brush + tool_type_radio_smart_fill); + window_width = std::max(window_width, 2.f * buttons_width + m_imgui->scaled(1.f)); + + auto draw_text_with_caption = [this, &caption_max](const wxString& caption, const wxString& text) { + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, caption); + ImGui::SameLine(caption_max); + m_imgui->text(text); + }; + + for (const auto &t : std::array{"enforce", "block", "remove"}) + draw_text_with_caption(m_desc.at(t + "_caption"), m_desc.at(t)); + + ImGui::Separator(); + + if (waiting_for_autogenerated_supports) { + m_imgui->text(m_desc.at("generating")); + } else { + bool generate = m_imgui->button(m_desc.at("auto_generate")); + if (generate) + auto_generate(); + } + ImGui::Separator(); + + float position_before_text_y = ImGui::GetCursorPos().y; + ImGui::AlignTextToFramePadding(); + m_imgui->text_wrapped(m_desc["highlight_by_angle"] + ":", autoset_slider_label_max_width); + ImGui::AlignTextToFramePadding(); + float position_after_text_y = ImGui::GetCursorPos().y; + std::string format_str = std::string("%.f") + I18N::translate_utf8("°", + "Degree sign to use in the respective slider in FDM supports gizmo," + "placed after the number with no whitespace in between."); + ImGui::SameLine(sliders_left_width); + + float slider_height = m_imgui->get_slider_float_height(); + // Makes slider to be aligned to bottom of the multi-line text. + float slider_start_position_y = std::max(position_before_text_y, position_after_text_y - slider_height); + ImGui::SetCursorPosY(slider_start_position_y); + + ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); + wxString tooltip = format_wxstr(_L("Preselects faces by overhang angle. It is possible to restrict paintable facets to only preselected faces when " + "the option \"%1%\" is enabled."), m_desc["on_overhangs_only"]); + if (m_imgui->slider_float("##angle_threshold_deg", &m_highlight_by_angle_threshold_deg, 0.f, 90.f, format_str.data(), 1.0f, true, tooltip)) { + m_parent.set_slope_normal_angle(90.f - m_highlight_by_angle_threshold_deg); + if (! m_parent.is_using_slope()) { + m_parent.use_slope(true); + m_parent.set_as_dirty(); + } + } + + // Restores the cursor position to be below the multi-line text. + ImGui::SetCursorPosY(std::max(position_before_text_y + slider_height, position_after_text_y)); + + const float max_tooltip_width = ImGui::GetFontSize() * 20.0f; + + m_imgui->disabled_begin(m_highlight_by_angle_threshold_deg == 0.f); + ImGui::NewLine(); + ImGui::SameLine(window_width - 2.f*buttons_width - m_imgui->scaled(0.5f)); + if (m_imgui->button(m_desc["enforce_button"], buttons_width, 0.f)) { + select_facets_by_angle(m_highlight_by_angle_threshold_deg, false); + m_highlight_by_angle_threshold_deg = 0.f; + m_parent.use_slope(false); + } + ImGui::SameLine(window_width - buttons_width); + if (m_imgui->button(m_desc["cancel"], buttons_width, 0.f)) { + m_highlight_by_angle_threshold_deg = 0.f; + m_parent.use_slope(false); + } + m_imgui->disabled_end(); + + + ImGui::Separator(); + + ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc["tool_type"]); + + float tool_type_offset = tool_type_radio_left + (window_width - tool_type_radio_left - tool_type_radio_brush - tool_type_radio_smart_fill + m_imgui->scaled(0.5f)) / 2.f; + ImGui::SameLine(tool_type_offset); + ImGui::PushItemWidth(tool_type_radio_brush); + if (m_imgui->radio_button(m_desc["tool_brush"], m_tool_type == ToolType::BRUSH)) + m_tool_type = ToolType::BRUSH; + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_L("Paints facets according to the chosen painting brush."), max_tooltip_width); + + ImGui::SameLine(tool_type_offset + tool_type_radio_brush); + ImGui::PushItemWidth(tool_type_radio_smart_fill); + if (m_imgui->radio_button(m_desc["tool_smart_fill"], m_tool_type == ToolType::SMART_FILL)) + m_tool_type = ToolType::SMART_FILL; + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_L("Paints neighboring facets whose relative angle is less or equal to set angle."), max_tooltip_width); + + m_imgui->checkbox(m_desc["on_overhangs_only"], m_paint_on_overhangs_only); + if (ImGui::IsItemHovered()) + m_imgui->tooltip(format_wxstr(_L("Allows painting only on facets selected by: \"%1%\""), m_desc["highlight_by_angle"]), max_tooltip_width); + + ImGui::Separator(); + + if (m_tool_type == ToolType::BRUSH) { + m_imgui->text(m_desc.at("cursor_type")); + ImGui::NewLine(); + + float cursor_type_offset = (window_width - cursor_type_radio_sphere - cursor_type_radio_circle - cursor_type_radio_pointer + m_imgui->scaled(1.5f)) / 2.f; + ImGui::SameLine(cursor_type_offset); + ImGui::PushItemWidth(cursor_type_radio_sphere); + if (m_imgui->radio_button(m_desc["sphere"], m_cursor_type == TriangleSelector::CursorType::SPHERE)) + m_cursor_type = TriangleSelector::CursorType::SPHERE; + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_L("Paints all facets inside, regardless of their orientation."), max_tooltip_width); + + ImGui::SameLine(cursor_type_offset + cursor_type_radio_sphere); + ImGui::PushItemWidth(cursor_type_radio_circle); + + if (m_imgui->radio_button(m_desc["circle"], m_cursor_type == TriangleSelector::CursorType::CIRCLE)) + m_cursor_type = TriangleSelector::CursorType::CIRCLE; + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_L("Ignores facets facing away from the camera."), max_tooltip_width); + + ImGui::SameLine(cursor_type_offset + cursor_type_radio_sphere + cursor_type_radio_circle); + ImGui::PushItemWidth(cursor_type_radio_pointer); + + if (m_imgui->radio_button(m_desc["pointer"], m_cursor_type == TriangleSelector::CursorType::POINTER)) + m_cursor_type = TriangleSelector::CursorType::POINTER; + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_L("Paints only one facet."), max_tooltip_width); + + m_imgui->disabled_begin(m_cursor_type != TriangleSelector::CursorType::SPHERE && m_cursor_type != TriangleSelector::CursorType::CIRCLE); + + ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc.at("cursor_size")); + ImGui::SameLine(sliders_left_width); + ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); + m_imgui->slider_float("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f", 1.0f, true, _L("Alt + Mouse wheel")); + + m_imgui->checkbox(m_desc["split_triangles"], m_triangle_splitting_enabled); + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_L("Splits bigger facets into smaller ones while the object is painted."), max_tooltip_width); + + m_imgui->disabled_end(); + } else { + assert(m_tool_type == ToolType::SMART_FILL); + ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc["smart_fill_angle"] + ":"); + + ImGui::SameLine(sliders_left_width); + ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); + if (m_imgui->slider_float("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str.data(), 1.0f, true, _L("Alt + Mouse wheel"))) + for (auto &triangle_selector : m_triangle_selectors) { + triangle_selector->seed_fill_unselect_all_triangles(); + triangle_selector->request_update_render_data(); + } + } + + ImGui::Separator(); + if (m_c->object_clipper()->get_position() == 0.f) { + ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc.at("clipping_of_view")); + } + else { + if (m_imgui->button(m_desc.at("reset_direction"))) { + wxGetApp().CallAfter([this](){ + m_c->object_clipper()->set_position_by_ratio(-1., false); + }); + } + } + + auto clp_dist = float(m_c->object_clipper()->get_position()); + ImGui::SameLine(sliders_left_width); + ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); + if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true, _L("Ctrl + Mouse wheel"))) + m_c->object_clipper()->set_position_by_ratio(clp_dist, true); + + ImGui::Separator(); + if (m_imgui->button(m_desc.at("remove_all"))) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Reset selection"), UndoRedo::SnapshotType::GizmoAction); + ModelObject *mo = m_c->selection_info()->model_object(); + int idx = -1; + for (ModelVolume *mv : mo->volumes) + if (mv->is_model_part()) { + ++idx; + m_triangle_selectors[idx]->reset(); + m_triangle_selectors[idx]->request_update_render_data(); + } + + update_model_object(); + this->waiting_for_autogenerated_supports = false; + m_parent.set_as_dirty(); + } + + m_imgui->end(); +} + + + +void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) +{ + float threshold = (float(M_PI)/180.f)*threshold_deg; + const Selection& selection = m_parent.get_selection(); + const ModelObject* mo = m_c->selection_info()->model_object(); + const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; + + int mesh_id = -1; + for (const ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; + + ++mesh_id; + +#if ENABLE_WORLD_COORDINATE + const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset(); +#else + const Transform3d trafo_matrix = mi->get_matrix(true) * mv->get_matrix(true); +#endif // ENABLE_WORLD_COORDINATE + Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast().normalized(); + Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); + + float dot_limit = limit.dot(down); + + // Now calculate dot product of vert_direction and facets' normals. + int idx = 0; + const indexed_triangle_set &its = mv->mesh().its; + for (const stl_triangle_vertex_indices &face : its.indices) { + if (its_face_normal(its, face).dot(down) > dot_limit) { + m_triangle_selectors[mesh_id]->set_facet(idx, block ? EnforcerBlockerType::BLOCKER : EnforcerBlockerType::ENFORCER); + m_triangle_selectors.back()->request_update_render_data(); + } + ++ idx; + } + } + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), block ? _L("Block supports by angle") + : _L("Add supports by angle")); + update_model_object(); + this->waiting_for_autogenerated_supports = false; + m_parent.set_as_dirty(); +} + +void GLGizmoFdmSupports::data_changed() +{ + GLGizmoPainterBase::data_changed(); + if (! m_c->selection_info()) + return; + + ModelObject* mo = m_c->selection_info()->model_object(); + if (mo && this->waiting_for_autogenerated_supports) { + get_data_from_backend(); + } else { + this->waiting_for_autogenerated_supports = false; + } +} + +void GLGizmoFdmSupports::get_data_from_backend() +{ + if (! has_backend_supports()) + return; + ModelObject* mo = m_c->selection_info()->model_object(); + + // find the respective PrintObject, we need a pointer to it + for (const PrintObject* po : m_parent.fff_print()->objects()) { + if (po->model_object()->id() == mo->id()) { + std::unordered_map mvs; + for (const ModelVolume* mv : po->model_object()->volumes) { + if (mv->is_model_part()) { + mvs.emplace(mv->id().id, mv); + } + } + int mesh_id = -1.0f; + for (ModelVolume* mv : mo->volumes){ + if (mv->is_model_part()){ + mesh_id++; + mv->supported_facets.assign(mvs[mv->id().id]->supported_facets); + m_triangle_selectors[mesh_id]->deserialize(mv->supported_facets.get_data(), true); + m_triangle_selectors[mesh_id]->request_update_render_data(); + } + } + this->waiting_for_autogenerated_supports = false; + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + m_parent.set_as_dirty(); + } + } +} + + +void GLGizmoFdmSupports::update_model_object() const +{ + bool updated = false; + ModelObject* mo = m_c->selection_info()->model_object(); + int idx = -1; + for (ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; + ++idx; + updated |= mv->supported_facets.set(*m_triangle_selectors[idx].get()); + } + + if (updated) { + const ModelObjectPtrs& mos = wxGetApp().model().objects; + wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin()); + + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + } +} + +void GLGizmoFdmSupports::update_from_model_object() +{ + wxBusyCursor wait; + + const ModelObject* mo = m_c->selection_info()->model_object(); + m_triangle_selectors.clear(); + + int volume_id = -1; + for (const ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; + + ++volume_id; + + // This mesh does not account for the possible Z up SLA offset. + const TriangleMesh* mesh = &mv->mesh(); + + m_triangle_selectors.emplace_back(std::make_unique(*mesh)); + // Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize(). + m_triangle_selectors.back()->deserialize(mv->supported_facets.get_data(), false); + m_triangle_selectors.back()->request_update_render_data(); + } +} + +bool GLGizmoFdmSupports::has_backend_supports() const +{ + const ModelObject* mo = m_c->selection_info()->model_object(); + if (! mo) + return false; + + // find PrintObject with this ID + for (const PrintObject* po : m_parent.fff_print()->objects()) { + if (po->model_object()->id() == mo->id()) + return po->is_step_done(posSupportSpotsSearch); + } + return false; +} + +void GLGizmoFdmSupports::reslice_FDM_supports(bool postpone_error_messages) const { + wxGetApp().CallAfter( + [this, postpone_error_messages]() { + wxGetApp().plater()->reslice_FFF_until_step(posSupportSpotsSearch, + *m_c->selection_info()->model_object(), postpone_error_messages); + }); +} + +void GLGizmoFdmSupports::auto_generate() +{ + ModelObject *mo = m_c->selection_info()->model_object(); + bool not_painted = std::all_of(mo->volumes.begin(), mo->volumes.end(), [](const ModelVolume* vol){ + return vol->type() != ModelVolumeType::MODEL_PART || vol->supported_facets.empty(); + }); + + MessageDialog dlg(GUI::wxGetApp().plater(), + _L("Autogeneration will erase all currently painted areas.") + "\n\n" + + _L("Are you sure you want to do it?") + "\n", + _L("Warning"), wxICON_WARNING | wxYES | wxNO); + + if (not_painted || dlg.ShowModal() == wxID_YES) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points")); + int mesh_id = -1.0f; + for (ModelVolume *mv : mo->volumes) { + if (mv->is_model_part()) { + mesh_id++; + mv->supported_facets.reset(); + m_triangle_selectors[mesh_id]->reset(); + m_triangle_selectors[mesh_id]->request_update_render_data(); + } + } + + mo->config.set("support_material", true); + mo->config.set("support_material_auto", false); + this->waiting_for_autogenerated_supports = true; + wxGetApp().CallAfter([this]() { reslice_FDM_supports(); }); + } +} + + +PainterGizmoType GLGizmoFdmSupports::get_painter_type() const +{ + return PainterGizmoType::FDM_SUPPORTS; +} + +wxString GLGizmoFdmSupports::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const +{ + wxString action_name; + if (shift_down) + action_name = _L("Remove selection"); + else { + if (button_down == Button::Left) + action_name = _L("Add supports"); + else + action_name = _L("Block supports"); + } + return action_name; +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp index bbae3f242..d21005e4c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp @@ -478,19 +478,19 @@ bool GLGizmoHollow::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_pos if (action == SLAGizmoEventType::MouseWheelUp && control_down) { double pos = m_c->object_clipper()->get_position(); pos = std::min(1., pos + 0.01); - m_c->object_clipper()->set_position(pos, true); + m_c->object_clipper()->set_position_by_ratio(pos, true); return true; } if (action == SLAGizmoEventType::MouseWheelDown && control_down) { double pos = m_c->object_clipper()->get_position(); pos = std::max(0., pos - 0.01); - m_c->object_clipper()->set_position(pos, true); + m_c->object_clipper()->set_position_by_ratio(pos, true); return true; } if (action == SLAGizmoEventType::ResetClippingPlane) { - m_c->object_clipper()->set_position(-1., false); + m_c->object_clipper()->set_position_by_ratio(-1., false); return true; } @@ -885,7 +885,7 @@ RENDER_AGAIN: else { if (m_imgui->button(m_desc.at("reset_direction"))) { wxGetApp().CallAfter([this](){ - m_c->object_clipper()->set_position(-1., false); + m_c->object_clipper()->set_position_by_ratio(-1., false); }); } } @@ -894,7 +894,7 @@ RENDER_AGAIN: ImGui::PushItemWidth(window_width - settings_sliders_left); float clp_dist = m_c->object_clipper()->get_position(); if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f")) - m_c->object_clipper()->set_position(clp_dist, true); + m_c->object_clipper()->set_position_by_ratio(clp_dist, true); // make sure supports are shown/hidden as appropriate bool show_sups = m_c->instances_hider()->are_supports_shown(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp index aa8cdda04..0fdc3b2db 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp @@ -31,7 +31,7 @@ public: void data_changed() override; bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down); void delete_selected_points(); - bool is_selection_rectangle_dragging() const { + bool is_selection_rectangle_dragging() const override { return m_selection_rectangle.is_dragging(); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index fd9f988da..e6c566d97 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -476,7 +476,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott m_imgui->text(m_desc.at("clipping_of_view")); } else { if (m_imgui->button(m_desc.at("reset_direction"))) { - wxGetApp().CallAfter([this]() { m_c->object_clipper()->set_position(-1., false); }); + wxGetApp().CallAfter([this]() { m_c->object_clipper()->set_position_by_ratio(-1., false); }); } } @@ -484,7 +484,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott ImGui::SameLine(sliders_left_width); ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true, _L("Ctrl + Mouse wheel"))) - m_c->object_clipper()->set_position(clp_dist, true); + m_c->object_clipper()->set_position_by_ratio(clp_dist, true); ImGui::Separator(); if (m_imgui->button(m_desc.at("remove_all"))) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp index 10b5c5870..12c783c43 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp @@ -1,485 +1,487 @@ -// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. -#include "GLGizmoMove.hpp" -#include "slic3r/GUI/GLCanvas3D.hpp" -#include "slic3r/GUI/GUI_App.hpp" -#if ENABLE_WORLD_COORDINATE -#include "slic3r/GUI/GUI_ObjectManipulation.hpp" -#endif // ENABLE_WORLD_COORDINATE -#if ENABLE_LEGACY_OPENGL_REMOVAL -#include "slic3r/GUI/Plater.hpp" -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#include - -#include - -namespace Slic3r { -namespace GUI { - -const double GLGizmoMove3D::Offset = 10.0; - -GLGizmoMove3D::GLGizmoMove3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) - : GLGizmoBase(parent, icon_filename, sprite_id) -{} - -std::string GLGizmoMove3D::get_tooltip() const -{ -#if ENABLE_WORLD_COORDINATE - if (m_hover_id == 0) - return "X: " + format(m_displacement.x(), 2); - else if (m_hover_id == 1) - return "Y: " + format(m_displacement.y(), 2); - else if (m_hover_id == 2) - return "Z: " + format(m_displacement.z(), 2); - else - return ""; -#else - const Selection& selection = m_parent.get_selection(); - const bool show_position = selection.is_single_full_instance(); - const Vec3d& position = selection.get_bounding_box().center(); - - if (m_hover_id == 0 || m_grabbers[0].dragging) - return "X: " + format(show_position ? position.x() : m_displacement.x(), 2); - else if (m_hover_id == 1 || m_grabbers[1].dragging) - return "Y: " + format(show_position ? position.y() : m_displacement.y(), 2); - else if (m_hover_id == 2 || m_grabbers[2].dragging) - return "Z: " + format(show_position ? position.z() : m_displacement.z(), 2); - else - return ""; -#endif // ENABLE_WORLD_COORDINATE -} - -bool GLGizmoMove3D::on_mouse(const wxMouseEvent &mouse_event) { - return use_grabbers(mouse_event); -} - -void GLGizmoMove3D::data_changed() { - m_grabbers[2].enabled = !m_parent.get_selection().is_wipe_tower(); -} - -bool GLGizmoMove3D::on_init() -{ - for (int i = 0; i < 3; ++i) { - m_grabbers.push_back(Grabber()); - m_grabbers.back().extensions = GLGizmoBase::EGrabberExtension::PosZ; - } - - m_grabbers[0].angles = { 0.0, 0.5 * double(PI), 0.0 }; - m_grabbers[1].angles = { -0.5 * double(PI), 0.0, 0.0 }; - - m_shortcut_key = WXK_CONTROL_M; - - return true; -} - -std::string GLGizmoMove3D::on_get_name() const -{ - return _u8L("Move"); -} - -bool GLGizmoMove3D::on_is_activable() const -{ - return !m_parent.get_selection().is_empty(); -} - -void GLGizmoMove3D::on_start_dragging() -{ - assert(m_hover_id != -1); - - m_displacement = Vec3d::Zero(); -#if ENABLE_WORLD_COORDINATE - const Selection& selection = m_parent.get_selection(); - const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); - if (coordinates_type == ECoordinatesType::World) - m_starting_drag_position = m_center + m_grabbers[m_hover_id].center; - else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { - const GLVolume& v = *selection.get_first_volume(); - m_starting_drag_position = m_center + v.get_instance_transformation().get_rotation_matrix() * v.get_volume_transformation().get_rotation_matrix() * m_grabbers[m_hover_id].center; - } - else { - const GLVolume& v = *selection.get_first_volume(); - m_starting_drag_position = m_center + v.get_instance_transformation().get_rotation_matrix() * m_grabbers[m_hover_id].center; - } - m_starting_box_center = m_center; - m_starting_box_bottom_center = m_center; - m_starting_box_bottom_center.z() = m_bounding_box.min.z(); -#else - const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); - m_starting_drag_position = m_grabbers[m_hover_id].center; - m_starting_box_center = box.center(); - m_starting_box_bottom_center = box.center(); - m_starting_box_bottom_center.z() = box.min.z(); -#endif // ENABLE_WORLD_COORDINATE -} - -void GLGizmoMove3D::on_stop_dragging() -{ - m_parent.do_move(L("Gizmo-Move")); - m_displacement = Vec3d::Zero(); -} - -void GLGizmoMove3D::on_dragging(const UpdateData& data) -{ - if (m_hover_id == 0) - m_displacement.x() = calc_projection(data); - else if (m_hover_id == 1) - m_displacement.y() = calc_projection(data); - else if (m_hover_id == 2) - m_displacement.z() = calc_projection(data); - - Selection &selection = m_parent.get_selection(); -#if ENABLE_WORLD_COORDINATE - TransformationType trafo_type; - trafo_type.set_relative(); - switch (wxGetApp().obj_manipul()->get_coordinates_type()) - { - case ECoordinatesType::Instance: { trafo_type.set_instance(); break; } - case ECoordinatesType::Local: { trafo_type.set_local(); break; } - default: { break; } - } - selection.translate(m_displacement, trafo_type); -#else - selection.translate(m_displacement); -#endif // ENABLE_WORLD_COORDINATE -} - -void GLGizmoMove3D::on_render() -{ - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); - glsafe(::glEnable(GL_DEPTH_TEST)); - -#if ENABLE_WORLD_COORDINATE -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPushMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - calc_selection_box_and_center(); -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Transform3d base_matrix = local_transform(m_parent.get_selection()); - for (int i = 0; i < 3; ++i) { - m_grabbers[i].matrix = base_matrix; - } -#else - transform_to_local(m_parent.get_selection()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - const Vec3d zero = Vec3d::Zero(); - const Vec3d half_box_size = 0.5 * m_bounding_box.size(); - - // x axis - m_grabbers[0].center = { half_box_size.x() + Offset, 0.0, 0.0 }; - m_grabbers[0].color = AXES_COLOR[0]; - - // y axis - m_grabbers[1].center = { 0.0, half_box_size.y() + Offset, 0.0 }; - m_grabbers[1].color = AXES_COLOR[1]; - - // z axis - m_grabbers[2].center = { 0.0, 0.0, half_box_size.z() + Offset }; - m_grabbers[2].color = AXES_COLOR[2]; -#else - const Selection& selection = m_parent.get_selection(); - const BoundingBoxf3& box = selection.get_bounding_box(); - const Vec3d& center = box.center(); - - // x axis - m_grabbers[0].center = { box.max.x() + Offset, center.y(), center.z() }; - m_grabbers[0].color = AXES_COLOR[0]; - - // y axis - m_grabbers[1].center = { center.x(), box.max.y() + Offset, center.z() }; - m_grabbers[1].color = AXES_COLOR[1]; - - // z axis - m_grabbers[2].center = { center.x(), center.y(), box.max.z() + Offset }; - m_grabbers[2].color = AXES_COLOR[2]; -#endif // ENABLE_WORLD_COORDINATE - -#if ENABLE_GL_CORE_PROFILE - if (!OpenGLManager::get_gl_info().is_core_profile()) -#endif // ENABLE_GL_CORE_PROFILE - glsafe(::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_WORLD_COORDINATE - auto render_grabber_connection = [this, &zero](unsigned int id) { -#else - auto render_grabber_connection = [this, ¢er](unsigned int id) { -#endif // ENABLE_WORLD_COORDINATE - if (m_grabbers[id].enabled) { -#if ENABLE_WORLD_COORDINATE - if (!m_grabber_connections[id].model.is_initialized() || !m_grabber_connections[id].old_center.isApprox(m_grabbers[id].center)) { - m_grabber_connections[id].old_center = m_grabbers[id].center; -#else - if (!m_grabber_connections[id].model.is_initialized() || !m_grabber_connections[id].old_center.isApprox(center)) { - m_grabber_connections[id].old_center = center; -#endif // ENABLE_WORLD_COORDINATE - m_grabber_connections[id].model.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.color = AXES_COLOR[id]; - init_data.reserve_vertices(2); - init_data.reserve_indices(2); - - // vertices -#if ENABLE_WORLD_COORDINATE - init_data.add_vertex((Vec3f)zero.cast()); -#else - init_data.add_vertex((Vec3f)center.cast()); -#endif // ENABLE_WORLD_COORDINATE - init_data.add_vertex((Vec3f)m_grabbers[id].center.cast()); - - // indices - init_data.add_line(0, 1); - - m_grabber_connections[id].model.init_from(std::move(init_data)); - } - - m_grabber_connections[id].model.render(); - } - }; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - if (m_hover_id == -1) { -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_GL_CORE_PROFILE - GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); -#else - GLShaderProgram* shader = wxGetApp().get_shader("flat"); -#endif // ENABLE_GL_CORE_PROFILE - if (shader != nullptr) { - shader->start_using(); - const Camera& camera = wxGetApp().plater()->get_camera(); -#if ENABLE_WORLD_COORDINATE - shader->set_uniform("view_model_matrix", camera.get_view_matrix() * base_matrix); -#else - shader->set_uniform("view_model_matrix", camera.get_view_matrix()); -#endif // ENABLE_WORLD_COORDINATE - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#if ENABLE_GL_CORE_PROFILE - const std::array& viewport = camera.get_viewport(); - shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); - shader->set_uniform("width", 0.25f); - shader->set_uniform("gap_size", 0.0f); -#endif // ENABLE_GL_CORE_PROFILE -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - // draw axes - for (unsigned int i = 0; i < 3; ++i) { -#if ENABLE_LEGACY_OPENGL_REMOVAL - render_grabber_connection(i); -#else - if (m_grabbers[i].enabled) { - glsafe(::glColor4fv(AXES_COLOR[i].data())); - ::glBegin(GL_LINES); -#if ENABLE_WORLD_COORDINATE - ::glVertex3dv(zero.data()); -#else - ::glVertex3dv(center.data()); -#endif // ENABLE_WORLD_COORDINATE - ::glVertex3dv(m_grabbers[i].center.data()); - glsafe(::glEnd()); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - // draw grabbers -#if ENABLE_WORLD_COORDINATE - render_grabbers(m_bounding_box); -#else - render_grabbers(box); -#endif // ENABLE_WORLD_COORDINATE - } - else { - // draw axis -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_GL_CORE_PROFILE - GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); -#else - GLShaderProgram* shader = wxGetApp().get_shader("flat"); -#endif // ENABLE_GL_CORE_PROFILE - if (shader != nullptr) { - shader->start_using(); - - const Camera& camera = wxGetApp().plater()->get_camera(); -#if ENABLE_WORLD_COORDINATE - shader->set_uniform("view_model_matrix", camera.get_view_matrix()* base_matrix); -#else - shader->set_uniform("view_model_matrix", camera.get_view_matrix()); -#endif // ENABLE_WORLD_COORDINATE - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#if ENABLE_GL_CORE_PROFILE - const std::array& viewport = camera.get_viewport(); - shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); - shader->set_uniform("width", 0.5f); - shader->set_uniform("gap_size", 0.0f); -#endif // ENABLE_GL_CORE_PROFILE - - render_grabber_connection(m_hover_id); - shader->stop_using(); - } - - shader = wxGetApp().get_shader("gouraud_light"); -#else - glsafe(::glColor4fv(AXES_COLOR[m_hover_id].data())); - ::glBegin(GL_LINES); -#if ENABLE_WORLD_COORDINATE - ::glVertex3dv(zero.data()); -#else - ::glVertex3dv(center.data()); -#endif // ENABLE_WORLD_COORDINATE - ::glVertex3dv(m_grabbers[m_hover_id].center.data()); - glsafe(::glEnd()); - - GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - if (shader != nullptr) { - shader->start_using(); - shader->set_uniform("emission_factor", 0.1f); - // draw grabber -#if ENABLE_WORLD_COORDINATE - const Vec3d box_size = m_bounding_box.size(); -#else - const Vec3d box_size = box.size(); -#endif // ENABLE_WORLD_COORDINATE - const float mean_size = (float)((box_size.x() + box_size.y() + box_size.z()) / 3.0); - m_grabbers[m_hover_id].render(true, mean_size); - shader->stop_using(); - } - } - -#if ENABLE_WORLD_COORDINATE -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPopMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL -#endif // ENABLE_WORLD_COORDINATE -} - -#if ENABLE_RAYCAST_PICKING -void GLGizmoMove3D::on_register_raycasters_for_picking() -{ - // the gizmo grabbers are rendered on top of the scene, so the raytraced picker should take it into account - m_parent.set_raycaster_gizmos_on_top(true); -} - -void GLGizmoMove3D::on_unregister_raycasters_for_picking() -{ - m_parent.set_raycaster_gizmos_on_top(false); -} -#else -void GLGizmoMove3D::on_render_for_picking() -{ - glsafe(::glDisable(GL_DEPTH_TEST)); - -#if ENABLE_WORLD_COORDINATE -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Transform3d base_matrix = local_transform(m_parent.get_selection()); - for (int i = 0; i < 3; ++i) { - m_grabbers[i].matrix = base_matrix; - } -#else - glsafe(::glPushMatrix()); - transform_to_local(m_parent.get_selection()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - render_grabbers_for_picking(m_bounding_box); -#if ENABLE_LEGACY_OPENGL_REMOVAL -#else - glsafe(::glPopMatrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#else - const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); - render_grabbers_for_picking(box); -#endif // ENABLE_WORLD_COORDINATE -} -#endif // ENABLE_RAYCAST_PICKING - -double GLGizmoMove3D::calc_projection(const UpdateData& data) const -{ - double projection = 0.0; - - const Vec3d starting_vec = m_starting_drag_position - m_starting_box_center; - const double len_starting_vec = starting_vec.norm(); - if (len_starting_vec != 0.0) { - const Vec3d mouse_dir = data.mouse_ray.unit_vector(); - // finds the intersection of the mouse ray with the plane parallel to the camera viewport and passing throught the starting position - // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebric form - // in our case plane normal and ray direction are the same (orthogonal view) - // when moving to perspective camera the negative z unit axis of the camera needs to be transformed in world space and used as plane normal - const Vec3d inters = data.mouse_ray.a + (m_starting_drag_position - data.mouse_ray.a).dot(mouse_dir) / mouse_dir.squaredNorm() * mouse_dir; - // vector from the starting position to the found intersection - const Vec3d inters_vec = inters - m_starting_drag_position; - - // finds projection of the vector along the staring direction - projection = inters_vec.dot(starting_vec.normalized()); - } - - if (wxGetKeyState(WXK_SHIFT)) - projection = m_snap_step * (double)std::round(projection / m_snap_step); - - return projection; -} - -#if ENABLE_WORLD_COORDINATE -#if ENABLE_LEGACY_OPENGL_REMOVAL -Transform3d GLGizmoMove3D::local_transform(const Selection& selection) const -{ - Transform3d ret = Geometry::assemble_transform(m_center); - if (!wxGetApp().obj_manipul()->is_world_coordinates()) { - const GLVolume& v = *selection.get_first_volume(); - Transform3d orient_matrix = v.get_instance_transformation().get_rotation_matrix(); - if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_local_coordinates()) - orient_matrix = orient_matrix * v.get_volume_transformation().get_rotation_matrix(); - ret = ret * orient_matrix; - } - return ret; -} -#else -void GLGizmoMove3D::transform_to_local(const Selection& selection) const -{ - glsafe(::glTranslated(m_center.x(), m_center.y(), m_center.z())); - - if (!wxGetApp().obj_manipul()->is_world_coordinates()) { - const GLVolume& v = *selection.get_first_volume(); - Transform3d orient_matrix = v.get_instance_transformation().get_matrix(true, false, true, true); - if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_local_coordinates()) - orient_matrix = orient_matrix * v.get_volume_transformation().get_matrix(true, false, true, true); - glsafe(::glMultMatrixd(orient_matrix.data())); - } -} -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -void GLGizmoMove3D::calc_selection_box_and_center() -{ - const Selection& selection = m_parent.get_selection(); - const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); - if (coordinates_type == ECoordinatesType::World) { - m_bounding_box = selection.get_bounding_box(); - m_center = m_bounding_box.center(); - } - else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { - const GLVolume& v = *selection.get_first_volume(); - m_bounding_box = v.transformed_convex_hull_bounding_box( - v.get_instance_transformation().get_scaling_factor_matrix() * v.get_volume_transformation().get_scaling_factor_matrix()); - m_center = v.world_matrix() * m_bounding_box.center(); - } - else { - m_bounding_box.reset(); - const Selection::IndicesList& ids = selection.get_volume_idxs(); - for (unsigned int id : ids) { - const GLVolume& v = *selection.get_volume(id); - m_bounding_box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); - } - const Geometry::Transformation inst_trafo = selection.get_first_volume()->get_instance_transformation(); - m_bounding_box = m_bounding_box.transformed(inst_trafo.get_scaling_factor_matrix()); - m_center = inst_trafo.get_matrix_no_scaling_factor() * m_bounding_box.center(); - } -} -#endif // ENABLE_WORLD_COORDINATE - -} // namespace GUI -} // namespace Slic3r +// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. +#include "GLGizmoMove.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#if ENABLE_WORLD_COORDINATE +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#endif // ENABLE_WORLD_COORDINATE +#if ENABLE_LEGACY_OPENGL_REMOVAL +#include "slic3r/GUI/Plater.hpp" +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#include "libslic3r/Model.hpp" + +#include + +#include + +namespace Slic3r { +namespace GUI { + +const double GLGizmoMove3D::Offset = 10.0; + +GLGizmoMove3D::GLGizmoMove3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoBase(parent, icon_filename, sprite_id) +{} + +std::string GLGizmoMove3D::get_tooltip() const +{ +#if ENABLE_WORLD_COORDINATE + if (m_hover_id == 0) + return "X: " + format(m_displacement.x(), 2); + else if (m_hover_id == 1) + return "Y: " + format(m_displacement.y(), 2); + else if (m_hover_id == 2) + return "Z: " + format(m_displacement.z(), 2); + else + return ""; +#else + const Selection& selection = m_parent.get_selection(); + const bool show_position = selection.is_single_full_instance(); + const Vec3d& position = selection.get_bounding_box().center(); + + if (m_hover_id == 0 || m_grabbers[0].dragging) + return "X: " + format(show_position ? position.x() : m_displacement.x(), 2); + else if (m_hover_id == 1 || m_grabbers[1].dragging) + return "Y: " + format(show_position ? position.y() : m_displacement.y(), 2); + else if (m_hover_id == 2 || m_grabbers[2].dragging) + return "Z: " + format(show_position ? position.z() : m_displacement.z(), 2); + else + return ""; +#endif // ENABLE_WORLD_COORDINATE +} + +bool GLGizmoMove3D::on_mouse(const wxMouseEvent &mouse_event) { + return use_grabbers(mouse_event); +} + +void GLGizmoMove3D::data_changed() { + m_grabbers[2].enabled = !m_parent.get_selection().is_wipe_tower(); +} + +bool GLGizmoMove3D::on_init() +{ + for (int i = 0; i < 3; ++i) { + m_grabbers.push_back(Grabber()); + m_grabbers.back().extensions = GLGizmoBase::EGrabberExtension::PosZ; + } + + m_grabbers[0].angles = { 0.0, 0.5 * double(PI), 0.0 }; + m_grabbers[1].angles = { -0.5 * double(PI), 0.0, 0.0 }; + + m_shortcut_key = WXK_CONTROL_M; + + return true; +} + +std::string GLGizmoMove3D::on_get_name() const +{ + return _u8L("Move"); +} + +bool GLGizmoMove3D::on_is_activable() const +{ + const Selection& selection = m_parent.get_selection(); + return !selection.is_any_cut_volume() && !selection.is_any_connector() && !selection.is_empty(); +} + +void GLGizmoMove3D::on_start_dragging() +{ + assert(m_hover_id != -1); + + m_displacement = Vec3d::Zero(); +#if ENABLE_WORLD_COORDINATE + const Selection& selection = m_parent.get_selection(); + const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (coordinates_type == ECoordinatesType::World) + m_starting_drag_position = m_center + m_grabbers[m_hover_id].center; + else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { + const GLVolume& v = *selection.get_first_volume(); + m_starting_drag_position = m_center + v.get_instance_transformation().get_rotation_matrix() * v.get_volume_transformation().get_rotation_matrix() * m_grabbers[m_hover_id].center; + } + else { + const GLVolume& v = *selection.get_first_volume(); + m_starting_drag_position = m_center + v.get_instance_transformation().get_rotation_matrix() * m_grabbers[m_hover_id].center; + } + m_starting_box_center = m_center; + m_starting_box_bottom_center = m_center; + m_starting_box_bottom_center.z() = m_bounding_box.min.z(); +#else + const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); + m_starting_drag_position = m_grabbers[m_hover_id].center; + m_starting_box_center = box.center(); + m_starting_box_bottom_center = box.center(); + m_starting_box_bottom_center.z() = box.min.z(); +#endif // ENABLE_WORLD_COORDINATE +} + +void GLGizmoMove3D::on_stop_dragging() +{ + m_parent.do_move(L("Gizmo-Move")); + m_displacement = Vec3d::Zero(); +} + +void GLGizmoMove3D::on_dragging(const UpdateData& data) +{ + if (m_hover_id == 0) + m_displacement.x() = calc_projection(data); + else if (m_hover_id == 1) + m_displacement.y() = calc_projection(data); + else if (m_hover_id == 2) + m_displacement.z() = calc_projection(data); + + Selection &selection = m_parent.get_selection(); +#if ENABLE_WORLD_COORDINATE + TransformationType trafo_type; + trafo_type.set_relative(); + switch (wxGetApp().obj_manipul()->get_coordinates_type()) + { + case ECoordinatesType::Instance: { trafo_type.set_instance(); break; } + case ECoordinatesType::Local: { trafo_type.set_local(); break; } + default: { break; } + } + selection.translate(m_displacement, trafo_type); +#else + selection.translate(m_displacement); +#endif // ENABLE_WORLD_COORDINATE +} + +void GLGizmoMove3D::on_render() +{ + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + glsafe(::glEnable(GL_DEPTH_TEST)); + +#if ENABLE_WORLD_COORDINATE +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPushMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + calc_selection_box_and_center(); +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Transform3d base_matrix = local_transform(m_parent.get_selection()); + for (int i = 0; i < 3; ++i) { + m_grabbers[i].matrix = base_matrix; + } +#else + transform_to_local(m_parent.get_selection()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + const Vec3d zero = Vec3d::Zero(); + const Vec3d half_box_size = 0.5 * m_bounding_box.size(); + + // x axis + m_grabbers[0].center = { half_box_size.x() + Offset, 0.0, 0.0 }; + m_grabbers[0].color = AXES_COLOR[0]; + + // y axis + m_grabbers[1].center = { 0.0, half_box_size.y() + Offset, 0.0 }; + m_grabbers[1].color = AXES_COLOR[1]; + + // z axis + m_grabbers[2].center = { 0.0, 0.0, half_box_size.z() + Offset }; + m_grabbers[2].color = AXES_COLOR[2]; +#else + const Selection& selection = m_parent.get_selection(); + const BoundingBoxf3& box = selection.get_bounding_box(); + const Vec3d& center = box.center(); + + // x axis + m_grabbers[0].center = { box.max.x() + Offset, center.y(), center.z() }; + m_grabbers[0].color = AXES_COLOR[0]; + + // y axis + m_grabbers[1].center = { center.x(), box.max.y() + Offset, center.z() }; + m_grabbers[1].color = AXES_COLOR[1]; + + // z axis + m_grabbers[2].center = { center.x(), center.y(), box.max.z() + Offset }; + m_grabbers[2].color = AXES_COLOR[2]; +#endif // ENABLE_WORLD_COORDINATE + +#if ENABLE_GL_CORE_PROFILE + if (!OpenGLManager::get_gl_info().is_core_profile()) +#endif // ENABLE_GL_CORE_PROFILE + glsafe(::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE + auto render_grabber_connection = [this, &zero](unsigned int id) { +#else + auto render_grabber_connection = [this, ¢er](unsigned int id) { +#endif // ENABLE_WORLD_COORDINATE + if (m_grabbers[id].enabled) { +#if ENABLE_WORLD_COORDINATE + if (!m_grabber_connections[id].model.is_initialized() || !m_grabber_connections[id].old_center.isApprox(m_grabbers[id].center)) { + m_grabber_connections[id].old_center = m_grabbers[id].center; +#else + if (!m_grabber_connections[id].model.is_initialized() || !m_grabber_connections[id].old_center.isApprox(center)) { + m_grabber_connections[id].old_center = center; +#endif // ENABLE_WORLD_COORDINATE + m_grabber_connections[id].model.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.color = AXES_COLOR[id]; + init_data.reserve_vertices(2); + init_data.reserve_indices(2); + + // vertices +#if ENABLE_WORLD_COORDINATE + init_data.add_vertex((Vec3f)zero.cast()); +#else + init_data.add_vertex((Vec3f)center.cast()); +#endif // ENABLE_WORLD_COORDINATE + init_data.add_vertex((Vec3f)m_grabbers[id].center.cast()); + + // indices + init_data.add_line(0, 1); + + m_grabber_connections[id].model.init_from(std::move(init_data)); + } + + m_grabber_connections[id].model.render(); + } + }; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + if (m_hover_id == -1) { +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_GL_CORE_PROFILE + GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); +#else + GLShaderProgram* shader = wxGetApp().get_shader("flat"); +#endif // ENABLE_GL_CORE_PROFILE + if (shader != nullptr) { + shader->start_using(); + const Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_WORLD_COORDINATE + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * base_matrix); +#else + shader->set_uniform("view_model_matrix", camera.get_view_matrix()); +#endif // ENABLE_WORLD_COORDINATE + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#if ENABLE_GL_CORE_PROFILE + const std::array& viewport = camera.get_viewport(); + shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); + shader->set_uniform("width", 0.25f); + shader->set_uniform("gap_size", 0.0f); +#endif // ENABLE_GL_CORE_PROFILE +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + // draw axes + for (unsigned int i = 0; i < 3; ++i) { +#if ENABLE_LEGACY_OPENGL_REMOVAL + render_grabber_connection(i); +#else + if (m_grabbers[i].enabled) { + glsafe(::glColor4fv(AXES_COLOR[i].data())); + ::glBegin(GL_LINES); +#if ENABLE_WORLD_COORDINATE + ::glVertex3dv(zero.data()); +#else + ::glVertex3dv(center.data()); +#endif // ENABLE_WORLD_COORDINATE + ::glVertex3dv(m_grabbers[i].center.data()); + glsafe(::glEnd()); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + shader->stop_using(); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + // draw grabbers +#if ENABLE_WORLD_COORDINATE + render_grabbers(m_bounding_box); +#else + render_grabbers(box); +#endif // ENABLE_WORLD_COORDINATE + } + else { + // draw axis +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_GL_CORE_PROFILE + GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); +#else + GLShaderProgram* shader = wxGetApp().get_shader("flat"); +#endif // ENABLE_GL_CORE_PROFILE + if (shader != nullptr) { + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_WORLD_COORDINATE + shader->set_uniform("view_model_matrix", camera.get_view_matrix()* base_matrix); +#else + shader->set_uniform("view_model_matrix", camera.get_view_matrix()); +#endif // ENABLE_WORLD_COORDINATE + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#if ENABLE_GL_CORE_PROFILE + const std::array& viewport = camera.get_viewport(); + shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); + shader->set_uniform("width", 0.5f); + shader->set_uniform("gap_size", 0.0f); +#endif // ENABLE_GL_CORE_PROFILE + + render_grabber_connection(m_hover_id); + shader->stop_using(); + } + + shader = wxGetApp().get_shader("gouraud_light"); +#else + glsafe(::glColor4fv(AXES_COLOR[m_hover_id].data())); + ::glBegin(GL_LINES); +#if ENABLE_WORLD_COORDINATE + ::glVertex3dv(zero.data()); +#else + ::glVertex3dv(center.data()); +#endif // ENABLE_WORLD_COORDINATE + ::glVertex3dv(m_grabbers[m_hover_id].center.data()); + glsafe(::glEnd()); + + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + if (shader != nullptr) { + shader->start_using(); + shader->set_uniform("emission_factor", 0.1f); + // draw grabber +#if ENABLE_WORLD_COORDINATE + const Vec3d box_size = m_bounding_box.size(); +#else + const Vec3d box_size = box.size(); +#endif // ENABLE_WORLD_COORDINATE + const float mean_size = (float)((box_size.x() + box_size.y() + box_size.z()) / 3.0); + m_grabbers[m_hover_id].render(true, mean_size); + shader->stop_using(); + } + } + +#if ENABLE_WORLD_COORDINATE +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL +#endif // ENABLE_WORLD_COORDINATE +} + +#if ENABLE_RAYCAST_PICKING +void GLGizmoMove3D::on_register_raycasters_for_picking() +{ + // the gizmo grabbers are rendered on top of the scene, so the raytraced picker should take it into account + m_parent.set_raycaster_gizmos_on_top(true); +} + +void GLGizmoMove3D::on_unregister_raycasters_for_picking() +{ + m_parent.set_raycaster_gizmos_on_top(false); +} +#else +void GLGizmoMove3D::on_render_for_picking() +{ + glsafe(::glDisable(GL_DEPTH_TEST)); + +#if ENABLE_WORLD_COORDINATE +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Transform3d base_matrix = local_transform(m_parent.get_selection()); + for (int i = 0; i < 3; ++i) { + m_grabbers[i].matrix = base_matrix; + } +#else + glsafe(::glPushMatrix()); + transform_to_local(m_parent.get_selection()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + render_grabbers_for_picking(m_bounding_box); +#if ENABLE_LEGACY_OPENGL_REMOVAL +#else + glsafe(::glPopMatrix()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#else + const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); + render_grabbers_for_picking(box); +#endif // ENABLE_WORLD_COORDINATE +} +#endif // ENABLE_RAYCAST_PICKING + +double GLGizmoMove3D::calc_projection(const UpdateData& data) const +{ + double projection = 0.0; + + const Vec3d starting_vec = m_starting_drag_position - m_starting_box_center; + const double len_starting_vec = starting_vec.norm(); + if (len_starting_vec != 0.0) { + const Vec3d mouse_dir = data.mouse_ray.unit_vector(); + // finds the intersection of the mouse ray with the plane parallel to the camera viewport and passing throught the starting position + // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebric form + // in our case plane normal and ray direction are the same (orthogonal view) + // when moving to perspective camera the negative z unit axis of the camera needs to be transformed in world space and used as plane normal + const Vec3d inters = data.mouse_ray.a + (m_starting_drag_position - data.mouse_ray.a).dot(mouse_dir) / mouse_dir.squaredNorm() * mouse_dir; + // vector from the starting position to the found intersection + const Vec3d inters_vec = inters - m_starting_drag_position; + + // finds projection of the vector along the staring direction + projection = inters_vec.dot(starting_vec.normalized()); + } + + if (wxGetKeyState(WXK_SHIFT)) + projection = m_snap_step * (double)std::round(projection / m_snap_step); + + return projection; +} + +#if ENABLE_WORLD_COORDINATE +#if ENABLE_LEGACY_OPENGL_REMOVAL +Transform3d GLGizmoMove3D::local_transform(const Selection& selection) const +{ + Transform3d ret = Geometry::assemble_transform(m_center); + if (!wxGetApp().obj_manipul()->is_world_coordinates()) { + const GLVolume& v = *selection.get_first_volume(); + Transform3d orient_matrix = v.get_instance_transformation().get_rotation_matrix(); + if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_local_coordinates()) + orient_matrix = orient_matrix * v.get_volume_transformation().get_rotation_matrix(); + ret = ret * orient_matrix; + } + return ret; +} +#else +void GLGizmoMove3D::transform_to_local(const Selection& selection) const +{ + glsafe(::glTranslated(m_center.x(), m_center.y(), m_center.z())); + + if (!wxGetApp().obj_manipul()->is_world_coordinates()) { + const GLVolume& v = *selection.get_first_volume(); + Transform3d orient_matrix = v.get_instance_transformation().get_matrix(true, false, true, true); + if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_local_coordinates()) + orient_matrix = orient_matrix * v.get_volume_transformation().get_matrix(true, false, true, true); + glsafe(::glMultMatrixd(orient_matrix.data())); + } +} +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +void GLGizmoMove3D::calc_selection_box_and_center() +{ + const Selection& selection = m_parent.get_selection(); + const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (coordinates_type == ECoordinatesType::World) { + m_bounding_box = selection.get_bounding_box(); + m_center = m_bounding_box.center(); + } + else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { + const GLVolume& v = *selection.get_first_volume(); + m_bounding_box = v.transformed_convex_hull_bounding_box( + v.get_instance_transformation().get_scaling_factor_matrix() * v.get_volume_transformation().get_scaling_factor_matrix()); + m_center = v.world_matrix() * m_bounding_box.center(); + } + else { + m_bounding_box.reset(); + const Selection::IndicesList& ids = selection.get_volume_idxs(); + for (unsigned int id : ids) { + const GLVolume& v = *selection.get_volume(id); + m_bounding_box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); + } + const Geometry::Transformation inst_trafo = selection.get_first_volume()->get_instance_transformation(); + m_bounding_box = m_bounding_box.transformed(inst_trafo.get_scaling_factor_matrix()); + m_center = inst_trafo.get_matrix_no_scaling_factor() * m_bounding_box.center(); + } +} +#endif // ENABLE_WORLD_COORDINATE + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 62611ac25..6640c5e06 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -537,7 +537,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous pos = action == SLAGizmoEventType::MouseWheelDown ? std::max(0., pos - 0.01) : std::min(1., pos + 0.01); - m_c->object_clipper()->set_position(pos, true); + m_c->object_clipper()->set_position_by_ratio(pos, true); return true; } else if (alt_down) { @@ -573,7 +573,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous } if (action == SLAGizmoEventType::ResetClippingPlane) { - m_c->object_clipper()->set_position(-1., false); + m_c->object_clipper()->set_position_by_ratio(-1., false); return true; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index db5c61869..80d6f1aa0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -1,999 +1,1001 @@ -// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. -#include "GLGizmoRotate.hpp" -#include "slic3r/GUI/GLCanvas3D.hpp" -#include "slic3r/GUI/ImGuiWrapper.hpp" -#if ENABLE_WORLD_COORDINATE -#include "slic3r/GUI/GUI_ObjectManipulation.hpp" -#endif // ENABLE_WORLD_COORDINATE - -#include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/GUI.hpp" -#include "slic3r/GUI/Plater.hpp" -#include "slic3r/GUI/Jobs/RotoptimizeJob.hpp" - -#include "libslic3r/PresetBundle.hpp" - -#include - -namespace Slic3r { -namespace GUI { - - -const float GLGizmoRotate::Offset = 5.0f; -const unsigned int GLGizmoRotate::AngleResolution = 64; -const unsigned int GLGizmoRotate::ScaleStepsCount = 72; -const float GLGizmoRotate::ScaleStepRad = 2.0f * float(PI) / GLGizmoRotate::ScaleStepsCount; -const unsigned int GLGizmoRotate::ScaleLongEvery = 2; -const float GLGizmoRotate::ScaleLongTooth = 0.1f; // in percent of radius -const unsigned int GLGizmoRotate::SnapRegionsCount = 8; -const float GLGizmoRotate::GrabberOffset = 0.15f; // in percent of radius - -GLGizmoRotate::GLGizmoRotate(GLCanvas3D& parent, GLGizmoRotate::Axis axis) - : GLGizmoBase(parent, "", -1) - , m_axis(axis) - , m_angle(0.0) - , m_center(0.0, 0.0, 0.0) - , m_radius(0.0f) - , m_snap_coarse_in_radius(0.0f) - , m_snap_coarse_out_radius(0.0f) - , m_snap_fine_in_radius(0.0f) - , m_snap_fine_out_radius(0.0f) - , m_drag_color(DEFAULT_DRAG_COLOR) - , m_highlight_color(DEFAULT_HIGHLIGHT_COLOR) -{ - m_group_id = static_cast(axis); -} - -void GLGizmoRotate::set_highlight_color(const ColorRGBA &color) -{ - m_highlight_color = color; -} - -void GLGizmoRotate::set_angle(double angle) -{ - if (std::abs(angle - 2.0 * double(PI)) < EPSILON) - angle = 0.0; - - m_angle = angle; -} - -std::string GLGizmoRotate::get_tooltip() const -{ - std::string axis; - switch (m_axis) - { - case X: { axis = "X"; break; } - case Y: { axis = "Y"; break; } - case Z: { axis = "Z"; break; } - } - return (m_hover_id == 0 || m_grabbers.front().dragging) ? axis + ": " + format(float(Geometry::rad2deg(m_angle)), 4) : ""; -} - -bool GLGizmoRotate::on_mouse(const wxMouseEvent &mouse_event) -{ - return use_grabbers(mouse_event); -} - -void GLGizmoRotate::dragging(const UpdateData &data) { on_dragging(data); } - -void GLGizmoRotate::start_dragging() -{ - m_grabbers[0].dragging = true; - on_start_dragging(); -} - -void GLGizmoRotate::stop_dragging() -{ - m_grabbers[0].dragging = false; - on_stop_dragging(); -} - -void GLGizmoRotate::enable_grabber() { m_grabbers[0].enabled = true; } -void GLGizmoRotate::disable_grabber() { m_grabbers[0].enabled = false; } - -bool GLGizmoRotate::on_init() -{ - m_grabbers.push_back(Grabber()); - m_grabbers.back().extensions = (GLGizmoBase::EGrabberExtension)(int(GLGizmoBase::EGrabberExtension::PosY) | int(GLGizmoBase::EGrabberExtension::NegY)); - return true; -} - -void GLGizmoRotate::on_start_dragging() -{ -#if ENABLE_WORLD_COORDINATE - init_data_from_selection(m_parent.get_selection()); -#else - const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); - m_center = box.center(); - m_radius = Offset + box.radius(); - m_snap_coarse_in_radius = m_radius / 3.0f; - m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; - m_snap_fine_in_radius = m_radius; - m_snap_fine_out_radius = m_snap_fine_in_radius + m_radius * ScaleLongTooth; -#endif // ENABLE_WORLD_COORDINATE -} - -void GLGizmoRotate::on_dragging(const UpdateData &data) -{ -#if ENABLE_WORLD_COORDINATE - const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane(data.mouse_ray)); -#else - const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane(data.mouse_ray, m_parent.get_selection())); -#endif // ENABLE_WORLD_COORDINATE - - const Vec2d orig_dir = Vec2d::UnitX(); - const Vec2d new_dir = mouse_pos.normalized(); - - double theta = ::acos(std::clamp(new_dir.dot(orig_dir), -1.0, 1.0)); - if (cross2(orig_dir, new_dir) < 0.0) - theta = 2.0 * (double)PI - theta; - - const double len = mouse_pos.norm(); - - // snap to coarse snap region - if (m_snap_coarse_in_radius <= len && len <= m_snap_coarse_out_radius) { - const double step = 2.0 * double(PI) / double(SnapRegionsCount); - theta = step * std::round(theta / step); - } - else { - // snap to fine snap region (scale) - if (m_snap_fine_in_radius <= len && len <= m_snap_fine_out_radius) { - const double step = 2.0 * double(PI) / double(ScaleStepsCount); - theta = step * std::round(theta / step); - } - } - - if (theta == 2.0 * double(PI)) - theta = 0.0; - - m_angle = theta; -} - -void GLGizmoRotate::on_render() -{ - if (!m_grabbers.front().enabled) - return; - - const Selection& selection = m_parent.get_selection(); -#if !ENABLE_WORLD_COORDINATE - const BoundingBoxf3& box = selection.get_bounding_box(); -#endif // !ENABLE_WORLD_COORDINATE - - if (m_hover_id != 0 && !m_grabbers.front().dragging) { -#if ENABLE_WORLD_COORDINATE - init_data_from_selection(selection); -#else - m_center = box.center(); - m_radius = Offset + box.radius(); - m_snap_coarse_in_radius = m_radius / 3.0f; - m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; - m_snap_fine_in_radius = m_radius; - m_snap_fine_out_radius = m_radius * (1.0f + ScaleLongTooth); -#endif // ENABLE_WORLD_COORDINATE - } - - const double grabber_radius = (double)m_radius * (1.0 + (double)GrabberOffset); - m_grabbers.front().center = Vec3d(::cos(m_angle) * grabber_radius, ::sin(m_angle) * grabber_radius, 0.0); - m_grabbers.front().angles.z() = m_angle; - - glsafe(::glEnable(GL_DEPTH_TEST)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_grabbers.front().matrix = local_transform(selection); -#else - glsafe(::glPushMatrix()); - transform_to_local(selection); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_GL_CORE_PROFILE - if (!OpenGLManager::get_gl_info().is_core_profile()) -#endif // ENABLE_GL_CORE_PROFILE - glsafe(::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f)); -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_GL_CORE_PROFILE - GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); -#else - GLShaderProgram* shader = wxGetApp().get_shader("flat"); -#endif // ENABLE_GL_CORE_PROFILE - if (shader != nullptr) { - shader->start_using(); - - const Camera& camera = wxGetApp().plater()->get_camera(); - const Transform3d view_model_matrix = camera.get_view_matrix() * m_grabbers.front().matrix; - shader->set_uniform("view_model_matrix", view_model_matrix); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#if ENABLE_GL_CORE_PROFILE - const std::array& viewport = camera.get_viewport(); - shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); - shader->set_uniform("width", 0.25f); - shader->set_uniform("gap_size", 0.0f); -#endif // ENABLE_GL_CORE_PROFILE - - const bool radius_changed = std::abs(m_old_radius - m_radius) > EPSILON; - m_old_radius = m_radius; - - const ColorRGBA color = (m_hover_id != -1) ? m_drag_color : m_highlight_color; - render_circle(color, radius_changed); - if (m_hover_id != -1) { - const bool hover_radius_changed = std::abs(m_old_hover_radius - m_radius) > EPSILON; - m_old_hover_radius = m_radius; - - render_scale(color, hover_radius_changed); - render_snap_radii(color, hover_radius_changed); - render_reference_radius(color, hover_radius_changed); - render_angle_arc(m_highlight_color, hover_radius_changed); - } - - render_grabber_connection(color, radius_changed); - shader->stop_using(); - } -#else - glsafe(::glColor4fv((m_hover_id != -1) ? m_drag_color.data() : m_highlight_color.data())); - - render_circle(); - - if (m_hover_id != -1) { - render_scale(); - render_snap_radii(); - render_reference_radius(); - } - - glsafe(::glColor4fv(m_highlight_color.data())); - - if (m_hover_id != -1) - render_angle(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_WORLD_COORDINATE - render_grabber(m_bounding_box); -#else - render_grabber(box); -#endif // ENABLE_WORLD_COORDINATE - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPopMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if !ENABLE_RAYCAST_PICKING -void GLGizmoRotate::on_render_for_picking() -{ - const Selection& selection = m_parent.get_selection(); - - glsafe(::glDisable(GL_DEPTH_TEST)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_grabbers.front().matrix = local_transform(selection); -#else - glsafe(::glPushMatrix()); - transform_to_local(selection); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_WORLD_COORDINATE - render_grabbers_for_picking(m_bounding_box); -#else - const BoundingBoxf3& box = selection.get_bounding_box(); - render_grabbers_for_picking(box); -#endif // ENABLE_WORLD_COORDINATE - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPopMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL -} -#endif // !ENABLE_RAYCAST_PICKING - -#if ENABLE_WORLD_COORDINATE -void GLGizmoRotate::init_data_from_selection(const Selection& selection) -{ - ECoordinatesType coordinates_type; - if (selection.is_wipe_tower()) - coordinates_type = ECoordinatesType::Local; - else - coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); - if (coordinates_type == ECoordinatesType::World) { - m_bounding_box = selection.get_bounding_box(); - m_center = m_bounding_box.center(); - } - else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { - const GLVolume& v = *selection.get_first_volume(); - m_bounding_box = v.transformed_convex_hull_bounding_box( - v.get_instance_transformation().get_scaling_factor_matrix() * v.get_volume_transformation().get_scaling_factor_matrix()); - m_center = v.world_matrix() * m_bounding_box.center(); - } - else { - m_bounding_box.reset(); - const Selection::IndicesList& ids = selection.get_volume_idxs(); - for (unsigned int id : ids) { - const GLVolume& v = *selection.get_volume(id); - m_bounding_box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); - } - const Geometry::Transformation inst_trafo = selection.get_first_volume()->get_instance_transformation(); - m_bounding_box = m_bounding_box.transformed(inst_trafo.get_scaling_factor_matrix()); - m_center = inst_trafo.get_matrix_no_scaling_factor() * m_bounding_box.center(); - } - - m_radius = Offset + m_bounding_box.radius(); - m_snap_coarse_in_radius = m_radius / 3.0f; - m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; - m_snap_fine_in_radius = m_radius; - m_snap_fine_out_radius = m_snap_fine_in_radius + m_radius * ScaleLongTooth; - - if (coordinates_type == ECoordinatesType::World) - m_orient_matrix = Transform3d::Identity(); - else if (coordinates_type == ECoordinatesType::Local && (selection.is_wipe_tower() || selection.is_single_volume_or_modifier())) { - const GLVolume& v = *selection.get_first_volume(); - m_orient_matrix = v.get_instance_transformation().get_rotation_matrix() * v.get_volume_transformation().get_rotation_matrix(); - } - else { - const GLVolume& v = *selection.get_first_volume(); - m_orient_matrix = v.get_instance_transformation().get_rotation_matrix(); - } -} -#endif // ENABLE_WORLD_COORDINATE - -void GLGizmoRotate3D::on_render_input_window(float x, float y, float bottom_limit) -{ - if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) - return; - - RotoptimzeWindow popup{m_imgui, m_rotoptimizewin_state, {x, y, bottom_limit}}; -} - -void GLGizmoRotate3D::load_rotoptimize_state() -{ - std::string accuracy_str = - wxGetApp().app_config->get("sla_auto_rotate", "accuracy"); - - std::string method_str = - wxGetApp().app_config->get("sla_auto_rotate", "method_id"); - - if (!accuracy_str.empty()) { - float accuracy = std::stof(accuracy_str); - accuracy = std::max(0.f, std::min(accuracy, 1.f)); - - m_rotoptimizewin_state.accuracy = accuracy; - } - - if (!method_str.empty()) { - int method_id = std::stoi(method_str); - if (method_id < int(RotoptimizeJob::get_methods_count())) - m_rotoptimizewin_state.method_id = method_id; - } -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLGizmoRotate::render_circle(const ColorRGBA& color, bool radius_changed) -#else -void GLGizmoRotate::render_circle() const -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_circle.is_initialized() || radius_changed) { - m_circle.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::LineLoop, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(ScaleStepsCount); - init_data.reserve_indices(ScaleStepsCount); - - // vertices + indices - for (unsigned int i = 0; i < ScaleStepsCount; ++i) { - const float angle = float(i * ScaleStepRad); - init_data.add_vertex(Vec3f(::cos(angle) * m_radius, ::sin(angle) * m_radius, 0.0f)); - init_data.add_index(i); - } - - m_circle.init_from(std::move(init_data)); - } - - m_circle.set_color(color); - m_circle.render(); -#else - ::glBegin(GL_LINE_LOOP); - for (unsigned int i = 0; i < ScaleStepsCount; ++i) { - const float angle = float(i) * ScaleStepRad; - const float x = ::cos(angle) * m_radius; - const float y = ::sin(angle) * m_radius; - const float z = 0.0f; - ::glVertex3f((GLfloat)x, (GLfloat)y, (GLfloat)z); - } - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLGizmoRotate::render_scale(const ColorRGBA& color, bool radius_changed) -#else -void GLGizmoRotate::render_scale() const -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ - const float out_radius_long = m_snap_fine_out_radius; - const float out_radius_short = m_radius * (1.0f + 0.5f * ScaleLongTooth); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_scale.is_initialized() || radius_changed) { - m_scale.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(2 * ScaleStepsCount); - init_data.reserve_indices(2 * ScaleStepsCount); - - // vertices + indices - for (unsigned int i = 0; i < ScaleStepsCount; ++i) { - const float angle = float(i * ScaleStepRad); - const float cosa = ::cos(angle); - const float sina = ::sin(angle); - const float in_x = cosa * m_radius; - const float in_y = sina * m_radius; - const float out_x = (i % ScaleLongEvery == 0) ? cosa * out_radius_long : cosa * out_radius_short; - const float out_y = (i % ScaleLongEvery == 0) ? sina * out_radius_long : sina * out_radius_short; - - // vertices - init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); - init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); - - // indices - init_data.add_line(i * 2, i * 2 + 1); - } - - m_scale.init_from(std::move(init_data)); - } - - m_scale.set_color(color); - m_scale.render(); -#else - ::glBegin(GL_LINES); - for (unsigned int i = 0; i < ScaleStepsCount; ++i) { - const float angle = (float)i * ScaleStepRad; - const float cosa = ::cos(angle); - const float sina = ::sin(angle); - const float in_x = cosa * m_radius; - const float in_y = sina * m_radius; - const float in_z = 0.0f; - const float out_x = (i % ScaleLongEvery == 0) ? cosa * out_radius_long : cosa * out_radius_short; - const float out_y = (i % ScaleLongEvery == 0) ? sina * out_radius_long : sina * out_radius_short; - const float out_z = 0.0f; - ::glVertex3f((GLfloat)in_x, (GLfloat)in_y, (GLfloat)in_z); - ::glVertex3f((GLfloat)out_x, (GLfloat)out_y, (GLfloat)out_z); - } - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLGizmoRotate::render_snap_radii(const ColorRGBA& color, bool radius_changed) -#else -void GLGizmoRotate::render_snap_radii() const -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ - const float step = 2.0f * float(PI) / float(SnapRegionsCount); - const float in_radius = m_radius / 3.0f; - const float out_radius = 2.0f * in_radius; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_snap_radii.is_initialized() || radius_changed) { - m_snap_radii.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(2 * ScaleStepsCount); - init_data.reserve_indices(2 * ScaleStepsCount); - - // vertices + indices - for (unsigned int i = 0; i < ScaleStepsCount; ++i) { - const float angle = float(i * step); - const float cosa = ::cos(angle); - const float sina = ::sin(angle); - const float in_x = cosa * in_radius; - const float in_y = sina * in_radius; - const float out_x = cosa * out_radius; - const float out_y = sina * out_radius; - - // vertices - init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); - init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); - - // indices - init_data.add_line(i * 2, i * 2 + 1); - } - - m_snap_radii.init_from(std::move(init_data)); - } - - m_snap_radii.set_color(color); - m_snap_radii.render(); -#else - ::glBegin(GL_LINES); - for (unsigned int i = 0; i < SnapRegionsCount; ++i) { - const float angle = (float)i * step; - const float cosa = ::cos(angle); - const float sina = ::sin(angle); - const float in_x = cosa * in_radius; - const float in_y = sina * in_radius; - const float in_z = 0.0f; - const float out_x = cosa * out_radius; - const float out_y = sina * out_radius; - const float out_z = 0.0f; - ::glVertex3f((GLfloat)in_x, (GLfloat)in_y, (GLfloat)in_z); - ::glVertex3f((GLfloat)out_x, (GLfloat)out_y, (GLfloat)out_z); - } - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLGizmoRotate::render_reference_radius(const ColorRGBA& color, bool radius_changed) -{ - if (!m_reference_radius.is_initialized() || radius_changed) { - m_reference_radius.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(2); - init_data.reserve_indices(2); - - // vertices - init_data.add_vertex(Vec3f(0.0f, 0.0f, 0.0f)); - init_data.add_vertex(Vec3f(m_radius * (1.0f + GrabberOffset), 0.0f, 0.0f)); - - // indices - init_data.add_line(0, 1); - - m_reference_radius.init_from(std::move(init_data)); - } - - m_reference_radius.set_color(color); - m_reference_radius.render(); -} -#else -void GLGizmoRotate::render_reference_radius() const -{ - ::glBegin(GL_LINES); - ::glVertex3f(0.0f, 0.0f, 0.0f); - ::glVertex3f((GLfloat)(m_radius * (1.0f + GrabberOffset)), 0.0f, 0.0f); - glsafe(::glEnd()); -} -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLGizmoRotate::render_angle_arc(const ColorRGBA& color, bool radius_changed) -#else -void GLGizmoRotate::render_angle() const -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ - const float step_angle = float(m_angle) / float(AngleResolution); - const float ex_radius = m_radius * (1.0f + GrabberOffset); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const bool angle_changed = std::abs(m_old_angle - m_angle) > EPSILON; - m_old_angle = m_angle; - - if (!m_angle_arc.is_initialized() || radius_changed || angle_changed) { - m_angle_arc.reset(); - if (m_angle > 0.0f) { - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::LineStrip, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(1 + AngleResolution); - init_data.reserve_indices(1 + AngleResolution); - - // vertices + indices - for (unsigned int i = 0; i <= AngleResolution; ++i) { - const float angle = float(i) * step_angle; - init_data.add_vertex(Vec3f(::cos(angle) * ex_radius, ::sin(angle) * ex_radius, 0.0f)); - init_data.add_index(i); - } - - m_angle_arc.init_from(std::move(init_data)); - } - } - - m_angle_arc.set_color(color); - m_angle_arc.render(); -#else - ::glBegin(GL_LINE_STRIP); - for (unsigned int i = 0; i <= AngleResolution; ++i) { - const float angle = float(i) * step_angle; - const float x = ::cos(angle) * ex_radius; - const float y = ::sin(angle) * ex_radius; - const float z = 0.0f; - ::glVertex3f((GLfloat)x, (GLfloat)y, (GLfloat)z); - } - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLGizmoRotate::render_grabber_connection(const ColorRGBA& color, bool radius_changed) -{ - if (!m_grabber_connection.model.is_initialized() || radius_changed || !m_grabber_connection.old_center.isApprox(m_grabbers.front().center)) { - m_grabber_connection.model.reset(); - m_grabber_connection.old_center = m_grabbers.front().center; - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(2); - init_data.reserve_indices(2); - - // vertices - init_data.add_vertex(Vec3f(0.0f, 0.0f, 0.0f)); - init_data.add_vertex((Vec3f)m_grabbers.front().center.cast()); - - // indices - init_data.add_line(0, 1); - - m_grabber_connection.model.init_from(std::move(init_data)); - } - - m_grabber_connection.model.set_color(color); - m_grabber_connection.model.render(); -} -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -void GLGizmoRotate::render_grabber(const BoundingBoxf3& box) -{ -#if !ENABLE_LEGACY_OPENGL_REMOVAL - const double grabber_radius = double(m_radius) * (1.0 + double(GrabberOffset)); - m_grabbers[0].center = Vec3d(::cos(m_angle) * grabber_radius, ::sin(m_angle) * grabber_radius, 0.0); - m_grabbers[0].angles.z() = m_angle; - - glsafe(::glColor4fv((m_hover_id != -1) ? m_drag_color.data() : m_highlight_color.data())); - - ::glBegin(GL_LINES); - ::glVertex3f(0.0f, 0.0f, 0.0f); - ::glVertex3dv(m_grabbers[0].center.data()); - glsafe(::glEnd()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - m_grabbers.front().color = m_highlight_color; - render_grabbers(box); -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -Transform3d GLGizmoRotate::local_transform(const Selection& selection) const -{ - Transform3d ret; - - switch (m_axis) - { - case X: - { -#if ENABLE_WORLD_COORDINATE - ret = Geometry::rotation_transform(0.5 * PI * Vec3d::UnitY()) * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()); -#else - ret = Geometry::assemble_transform(Vec3d::Zero(), 0.5 * PI * Vec3d::UnitY()) * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()); -#endif // ENABLE_WORLD_COORDINATE - break; - } - case Y: - { -#if ENABLE_WORLD_COORDINATE - ret = Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()) * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitY()); -#else - ret = Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()) * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitY()); -#endif // ENABLE_WORLD_COORDINATE - break; - } - default: - case Z: - { - ret = Transform3d::Identity(); - break; - } - } - -#if ENABLE_WORLD_COORDINATE - return Geometry::translation_transform(m_center) * m_orient_matrix * ret; -#else - if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) - ret = selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true) * ret; - - return Geometry::assemble_transform(m_center) * ret; -#endif // ENABLE_WORLD_COORDINATE -} -#else -void GLGizmoRotate::transform_to_local(const Selection& selection) const -{ - glsafe(::glTranslated(m_center.x(), m_center.y(), m_center.z())); - -#if ENABLE_WORLD_COORDINATE - glsafe(::glMultMatrixd(m_orient_matrix.data())); -#else - if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) { - const Transform3d orient_matrix = selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true); - glsafe(::glMultMatrixd(orient_matrix.data())); - } -#endif // ENABLE_WORLD_COORDINATE - - switch (m_axis) - { - case X: - { - glsafe(::glRotatef(90.0f, 0.0f, 1.0f, 0.0f)); - glsafe(::glRotatef(-90.0f, 0.0f, 0.0f, 1.0f)); - break; - } - case Y: - { - glsafe(::glRotatef(-90.0f, 0.0f, 0.0f, 1.0f)); - glsafe(::glRotatef(-90.0f, 0.0f, 1.0f, 0.0f)); - break; - } - default: - case Z: - { - // no rotation - break; - } - } -} -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_WORLD_COORDINATE -Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray) const -#else -Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray, const Selection& selection) const -#endif // ENABLE_WORLD_COORDINATE -{ - double half_pi = 0.5 * double(PI); - - Transform3d m = Transform3d::Identity(); - - switch (m_axis) - { - case X: - { - m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); - m.rotate(Eigen::AngleAxisd(-half_pi, Vec3d::UnitY())); - break; - } - case Y: - { - m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitY())); - m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); - break; - } - default: - case Z: - { - // no rotation applied - break; - } - } - -#if ENABLE_WORLD_COORDINATE - m = m * m_orient_matrix.inverse(); -#else - if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) - m = m * selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true).inverse(); -#endif // ENABLE_WORLD_COORDINATE - - m.translate(-m_center); - - const Linef3 local_mouse_ray = transform(mouse_ray, m); - if (std::abs(local_mouse_ray.vector().dot(Vec3d::UnitZ())) < EPSILON) { - // if the ray is parallel to the plane containing the circle - if (std::abs(local_mouse_ray.vector().dot(Vec3d::UnitY())) > 1.0 - EPSILON) - // if the ray is parallel to grabber direction - return Vec3d::UnitX(); - else { - const Vec3d world_pos = (local_mouse_ray.a.x() >= 0.0) ? mouse_ray.a - m_center : mouse_ray.b - m_center; - m.translate(m_center); - return m * world_pos; - } - } - else - return local_mouse_ray.intersect_plane(0.0); -} - -GLGizmoRotate3D::GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) - : GLGizmoBase(parent, icon_filename, sprite_id) - , m_gizmos({ - GLGizmoRotate(parent, GLGizmoRotate::X), - GLGizmoRotate(parent, GLGizmoRotate::Y), - GLGizmoRotate(parent, GLGizmoRotate::Z) }) -{ - load_rotoptimize_state(); -} - -bool GLGizmoRotate3D::on_mouse(const wxMouseEvent &mouse_event) -{ - if (mouse_event.Dragging() && m_dragging) { - // Apply new temporary rotations -#if ENABLE_WORLD_COORDINATE - TransformationType transformation_type; - if (m_parent.get_selection().is_wipe_tower()) - transformation_type = TransformationType::Instance_Relative_Joint; - else { - switch (wxGetApp().obj_manipul()->get_coordinates_type()) - { - default: - case ECoordinatesType::World: { transformation_type = TransformationType::World_Relative_Joint; break; } - case ECoordinatesType::Instance: { transformation_type = TransformationType::Instance_Relative_Joint; break; } - case ECoordinatesType::Local: { transformation_type = TransformationType::Local_Relative_Joint; break; } - } - } -#else - TransformationType transformation_type(TransformationType::World_Relative_Joint); -#endif // ENABLE_WORLD_COORDINATE - if (mouse_event.AltDown()) - transformation_type.set_independent(); - m_parent.get_selection().rotate(get_rotation(), transformation_type); - } - return use_grabbers(mouse_event); -} - -void GLGizmoRotate3D::data_changed() { - if (m_parent.get_selection().is_wipe_tower()) { -#if !ENABLE_WORLD_COORDINATE - const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - const float wipe_tower_rotation_angle = - dynamic_cast( - config.option("wipe_tower_rotation_angle"))->value; - set_rotation(Vec3d(0., 0., (M_PI / 180.) * wipe_tower_rotation_angle)); -#endif // !ENABLE_WORLD_COORDINATE - m_gizmos[0].disable_grabber(); - m_gizmos[1].disable_grabber(); - } - else { -#if !ENABLE_WORLD_COORDINATE - set_rotation(Vec3d::Zero()); -#endif // !ENABLE_WORLD_COORDINATE - m_gizmos[0].enable_grabber(); - m_gizmos[1].enable_grabber(); - } -#if ENABLE_WORLD_COORDINATE - set_rotation(Vec3d::Zero()); -#endif // ENABLE_WORLD_COORDINATE -} - -bool GLGizmoRotate3D::on_init() -{ - for (GLGizmoRotate& g : m_gizmos) - if (!g.init()) return false; - - for (unsigned int i = 0; i < 3; ++i) - m_gizmos[i].set_highlight_color(AXES_COLOR[i]); - - m_shortcut_key = WXK_CONTROL_R; - - return true; -} - -std::string GLGizmoRotate3D::on_get_name() const -{ - return _u8L("Rotate"); -} - -bool GLGizmoRotate3D::on_is_activable() const -{ - return !m_parent.get_selection().is_empty(); -} - -void GLGizmoRotate3D::on_start_dragging() -{ - assert(0 <= m_hover_id && m_hover_id < 3); - m_gizmos[m_hover_id].start_dragging(); -} - -void GLGizmoRotate3D::on_stop_dragging() -{ - assert(0 <= m_hover_id && m_hover_id < 3); - m_parent.do_rotate(L("Gizmo-Rotate")); - m_gizmos[m_hover_id].stop_dragging(); -} - -void GLGizmoRotate3D::on_dragging(const UpdateData &data) -{ - assert(0 <= m_hover_id && m_hover_id < 3); - m_gizmos[m_hover_id].dragging(data); -} - -void GLGizmoRotate3D::on_render() -{ - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); - - if (m_hover_id == -1 || m_hover_id == 0) - m_gizmos[X].render(); - - if (m_hover_id == -1 || m_hover_id == 1) - m_gizmos[Y].render(); - - if (m_hover_id == -1 || m_hover_id == 2) - m_gizmos[Z].render(); -} - -#if ENABLE_RAYCAST_PICKING -void GLGizmoRotate3D::on_register_raycasters_for_picking() -{ - // the gizmo grabbers are rendered on top of the scene, so the raytraced picker should take it into account - m_parent.set_raycaster_gizmos_on_top(true); - for (GLGizmoRotate& g : m_gizmos) { - g.register_raycasters_for_picking(); - } -} - -void GLGizmoRotate3D::on_unregister_raycasters_for_picking() -{ - for (GLGizmoRotate& g : m_gizmos) { - g.unregister_raycasters_for_picking(); - } - m_parent.set_raycaster_gizmos_on_top(false); -} -#endif // ENABLE_RAYCAST_PICKING - -GLGizmoRotate3D::RotoptimzeWindow::RotoptimzeWindow(ImGuiWrapper * imgui, - State & state, - const Alignment &alignment) - : m_imgui{imgui} -{ - imgui->begin(_L("Optimize orientation"), ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoCollapse); - - // adjust window position to avoid overlap the view toolbar - float win_h = ImGui::GetWindowHeight(); - float x = alignment.x, y = alignment.y; - y = std::min(y, alignment.bottom_limit - win_h); - ImGui::SetWindowPos(ImVec2(x, y), ImGuiCond_Always); - - float max_text_w = 0.; - auto padding = ImGui::GetStyle().FramePadding; - padding.x *= 2.f; - padding.y *= 2.f; - - for (size_t i = 0; i < RotoptimizeJob::get_methods_count(); ++i) { - float w = - ImGui::CalcTextSize(RotoptimizeJob::get_method_name(i).c_str()).x + - padding.x + ImGui::GetFrameHeight(); - max_text_w = std::max(w, max_text_w); - } - - ImGui::PushItemWidth(max_text_w); - - if (ImGui::BeginCombo("", RotoptimizeJob::get_method_name(state.method_id).c_str())) { - for (size_t i = 0; i < RotoptimizeJob::get_methods_count(); ++i) { - if (ImGui::Selectable(RotoptimizeJob::get_method_name(i).c_str())) { - state.method_id = i; - wxGetApp().app_config->set("sla_auto_rotate", - "method_id", - std::to_string(state.method_id)); - } - - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", RotoptimizeJob::get_method_description(i).c_str()); - } - - ImGui::EndCombo(); - } - - ImVec2 sz = ImGui::GetItemRectSize(); - - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", RotoptimizeJob::get_method_description(state.method_id).c_str()); - - ImGui::Separator(); - - auto btn_txt = _L("Apply"); - auto btn_txt_sz = ImGui::CalcTextSize(btn_txt.c_str()); - ImVec2 button_sz = {btn_txt_sz.x + padding.x, btn_txt_sz.y + padding.y}; - ImGui::SetCursorPosX(padding.x + sz.x - button_sz.x); - - if (!wxGetApp().plater()->get_ui_job_worker().is_idle()) - imgui->disabled_begin(true); - - if ( imgui->button(btn_txt) ) { - replace_job(wxGetApp().plater()->get_ui_job_worker(), - std::make_unique()); - } - - imgui->disabled_end(); -} - -GLGizmoRotate3D::RotoptimzeWindow::~RotoptimzeWindow() -{ - m_imgui->end(); -} - -} // namespace GUI -} // namespace Slic3r +// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. +#include "GLGizmoRotate.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/ImGuiWrapper.hpp" +#if ENABLE_WORLD_COORDINATE +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#endif // ENABLE_WORLD_COORDINATE + +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/Jobs/RotoptimizeJob.hpp" + +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Model.hpp" + +#include + +namespace Slic3r { +namespace GUI { + + +const float GLGizmoRotate::Offset = 5.0f; +const unsigned int GLGizmoRotate::AngleResolution = 64; +const unsigned int GLGizmoRotate::ScaleStepsCount = 72; +const float GLGizmoRotate::ScaleStepRad = 2.0f * float(PI) / GLGizmoRotate::ScaleStepsCount; +const unsigned int GLGizmoRotate::ScaleLongEvery = 2; +const float GLGizmoRotate::ScaleLongTooth = 0.1f; // in percent of radius +const unsigned int GLGizmoRotate::SnapRegionsCount = 8; +const float GLGizmoRotate::GrabberOffset = 0.15f; // in percent of radius + +GLGizmoRotate::GLGizmoRotate(GLCanvas3D& parent, GLGizmoRotate::Axis axis) + : GLGizmoBase(parent, "", -1) + , m_axis(axis) + , m_angle(0.0) + , m_center(0.0, 0.0, 0.0) + , m_radius(0.0f) + , m_snap_coarse_in_radius(0.0f) + , m_snap_coarse_out_radius(0.0f) + , m_snap_fine_in_radius(0.0f) + , m_snap_fine_out_radius(0.0f) + , m_drag_color(DEFAULT_DRAG_COLOR) + , m_highlight_color(DEFAULT_HIGHLIGHT_COLOR) +{ + m_group_id = static_cast(axis); +} + +void GLGizmoRotate::set_highlight_color(const ColorRGBA &color) +{ + m_highlight_color = color; +} + +void GLGizmoRotate::set_angle(double angle) +{ + if (std::abs(angle - 2.0 * double(PI)) < EPSILON) + angle = 0.0; + + m_angle = angle; +} + +std::string GLGizmoRotate::get_tooltip() const +{ + std::string axis; + switch (m_axis) + { + case X: { axis = "X"; break; } + case Y: { axis = "Y"; break; } + case Z: { axis = "Z"; break; } + } + return (m_hover_id == 0 || m_grabbers.front().dragging) ? axis + ": " + format(float(Geometry::rad2deg(m_angle)), 4) : ""; +} + +bool GLGizmoRotate::on_mouse(const wxMouseEvent &mouse_event) +{ + return use_grabbers(mouse_event); +} + +void GLGizmoRotate::dragging(const UpdateData &data) { on_dragging(data); } + +void GLGizmoRotate::start_dragging() +{ + m_grabbers[0].dragging = true; + on_start_dragging(); +} + +void GLGizmoRotate::stop_dragging() +{ + m_grabbers[0].dragging = false; + on_stop_dragging(); +} + +void GLGizmoRotate::enable_grabber() { m_grabbers[0].enabled = true; } +void GLGizmoRotate::disable_grabber() { m_grabbers[0].enabled = false; } + +bool GLGizmoRotate::on_init() +{ + m_grabbers.push_back(Grabber()); + m_grabbers.back().extensions = (GLGizmoBase::EGrabberExtension)(int(GLGizmoBase::EGrabberExtension::PosY) | int(GLGizmoBase::EGrabberExtension::NegY)); + return true; +} + +void GLGizmoRotate::on_start_dragging() +{ +#if ENABLE_WORLD_COORDINATE + init_data_from_selection(m_parent.get_selection()); +#else + const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); + m_center = box.center(); + m_radius = Offset + box.radius(); + m_snap_coarse_in_radius = m_radius / 3.0f; + m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; + m_snap_fine_in_radius = m_radius; + m_snap_fine_out_radius = m_snap_fine_in_radius + m_radius * ScaleLongTooth; +#endif // ENABLE_WORLD_COORDINATE +} + +void GLGizmoRotate::on_dragging(const UpdateData &data) +{ +#if ENABLE_WORLD_COORDINATE + const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane(data.mouse_ray)); +#else + const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane(data.mouse_ray, m_parent.get_selection())); +#endif // ENABLE_WORLD_COORDINATE + + const Vec2d orig_dir = Vec2d::UnitX(); + const Vec2d new_dir = mouse_pos.normalized(); + + double theta = ::acos(std::clamp(new_dir.dot(orig_dir), -1.0, 1.0)); + if (cross2(orig_dir, new_dir) < 0.0) + theta = 2.0 * (double)PI - theta; + + const double len = mouse_pos.norm(); + + // snap to coarse snap region + if (m_snap_coarse_in_radius <= len && len <= m_snap_coarse_out_radius) { + const double step = 2.0 * double(PI) / double(SnapRegionsCount); + theta = step * std::round(theta / step); + } + else { + // snap to fine snap region (scale) + if (m_snap_fine_in_radius <= len && len <= m_snap_fine_out_radius) { + const double step = 2.0 * double(PI) / double(ScaleStepsCount); + theta = step * std::round(theta / step); + } + } + + if (theta == 2.0 * double(PI)) + theta = 0.0; + + m_angle = theta; +} + +void GLGizmoRotate::on_render() +{ + if (!m_grabbers.front().enabled) + return; + + const Selection& selection = m_parent.get_selection(); +#if !ENABLE_WORLD_COORDINATE + const BoundingBoxf3& box = selection.get_bounding_box(); +#endif // !ENABLE_WORLD_COORDINATE + + if (m_hover_id != 0 && !m_grabbers.front().dragging) { +#if ENABLE_WORLD_COORDINATE + init_data_from_selection(selection); +#else + m_center = box.center(); + m_radius = Offset + box.radius(); + m_snap_coarse_in_radius = m_radius / 3.0f; + m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; + m_snap_fine_in_radius = m_radius; + m_snap_fine_out_radius = m_radius * (1.0f + ScaleLongTooth); +#endif // ENABLE_WORLD_COORDINATE + } + + const double grabber_radius = (double)m_radius * (1.0 + (double)GrabberOffset); + m_grabbers.front().center = Vec3d(::cos(m_angle) * grabber_radius, ::sin(m_angle) * grabber_radius, 0.0); + m_grabbers.front().angles.z() = m_angle; + + glsafe(::glEnable(GL_DEPTH_TEST)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_grabbers.front().matrix = local_transform(selection); +#else + glsafe(::glPushMatrix()); + transform_to_local(selection); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_GL_CORE_PROFILE + if (!OpenGLManager::get_gl_info().is_core_profile()) +#endif // ENABLE_GL_CORE_PROFILE + glsafe(::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f)); +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_GL_CORE_PROFILE + GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); +#else + GLShaderProgram* shader = wxGetApp().get_shader("flat"); +#endif // ENABLE_GL_CORE_PROFILE + if (shader != nullptr) { + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d view_model_matrix = camera.get_view_matrix() * m_grabbers.front().matrix; + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#if ENABLE_GL_CORE_PROFILE + const std::array& viewport = camera.get_viewport(); + shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); + shader->set_uniform("width", 0.25f); + shader->set_uniform("gap_size", 0.0f); +#endif // ENABLE_GL_CORE_PROFILE + + const bool radius_changed = std::abs(m_old_radius - m_radius) > EPSILON; + m_old_radius = m_radius; + + const ColorRGBA color = (m_hover_id != -1) ? m_drag_color : m_highlight_color; + render_circle(color, radius_changed); + if (m_hover_id != -1) { + const bool hover_radius_changed = std::abs(m_old_hover_radius - m_radius) > EPSILON; + m_old_hover_radius = m_radius; + + render_scale(color, hover_radius_changed); + render_snap_radii(color, hover_radius_changed); + render_reference_radius(color, hover_radius_changed); + render_angle_arc(m_highlight_color, hover_radius_changed); + } + + render_grabber_connection(color, radius_changed); + shader->stop_using(); + } +#else + glsafe(::glColor4fv((m_hover_id != -1) ? m_drag_color.data() : m_highlight_color.data())); + + render_circle(); + + if (m_hover_id != -1) { + render_scale(); + render_snap_radii(); + render_reference_radius(); + } + + glsafe(::glColor4fv(m_highlight_color.data())); + + if (m_hover_id != -1) + render_angle(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_WORLD_COORDINATE + render_grabber(m_bounding_box); +#else + render_grabber(box); +#endif // ENABLE_WORLD_COORDINATE + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if !ENABLE_RAYCAST_PICKING +void GLGizmoRotate::on_render_for_picking() +{ + const Selection& selection = m_parent.get_selection(); + + glsafe(::glDisable(GL_DEPTH_TEST)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_grabbers.front().matrix = local_transform(selection); +#else + glsafe(::glPushMatrix()); + transform_to_local(selection); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_WORLD_COORDINATE + render_grabbers_for_picking(m_bounding_box); +#else + const BoundingBoxf3& box = selection.get_bounding_box(); + render_grabbers_for_picking(box); +#endif // ENABLE_WORLD_COORDINATE + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL +} +#endif // !ENABLE_RAYCAST_PICKING + +#if ENABLE_WORLD_COORDINATE +void GLGizmoRotate::init_data_from_selection(const Selection& selection) +{ + ECoordinatesType coordinates_type; + if (selection.is_wipe_tower()) + coordinates_type = ECoordinatesType::Local; + else + coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (coordinates_type == ECoordinatesType::World) { + m_bounding_box = selection.get_bounding_box(); + m_center = m_bounding_box.center(); + } + else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { + const GLVolume& v = *selection.get_first_volume(); + m_bounding_box = v.transformed_convex_hull_bounding_box( + v.get_instance_transformation().get_scaling_factor_matrix() * v.get_volume_transformation().get_scaling_factor_matrix()); + m_center = v.world_matrix() * m_bounding_box.center(); + } + else { + m_bounding_box.reset(); + const Selection::IndicesList& ids = selection.get_volume_idxs(); + for (unsigned int id : ids) { + const GLVolume& v = *selection.get_volume(id); + m_bounding_box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); + } + const Geometry::Transformation inst_trafo = selection.get_first_volume()->get_instance_transformation(); + m_bounding_box = m_bounding_box.transformed(inst_trafo.get_scaling_factor_matrix()); + m_center = inst_trafo.get_matrix_no_scaling_factor() * m_bounding_box.center(); + } + + m_radius = Offset + m_bounding_box.radius(); + m_snap_coarse_in_radius = m_radius / 3.0f; + m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; + m_snap_fine_in_radius = m_radius; + m_snap_fine_out_radius = m_snap_fine_in_radius + m_radius * ScaleLongTooth; + + if (coordinates_type == ECoordinatesType::World) + m_orient_matrix = Transform3d::Identity(); + else if (coordinates_type == ECoordinatesType::Local && (selection.is_wipe_tower() || selection.is_single_volume_or_modifier())) { + const GLVolume& v = *selection.get_first_volume(); + m_orient_matrix = v.get_instance_transformation().get_rotation_matrix() * v.get_volume_transformation().get_rotation_matrix(); + } + else { + const GLVolume& v = *selection.get_first_volume(); + m_orient_matrix = v.get_instance_transformation().get_rotation_matrix(); + } +} +#endif // ENABLE_WORLD_COORDINATE + +void GLGizmoRotate3D::on_render_input_window(float x, float y, float bottom_limit) +{ + if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) + return; + + RotoptimzeWindow popup{m_imgui, m_rotoptimizewin_state, {x, y, bottom_limit}}; +} + +void GLGizmoRotate3D::load_rotoptimize_state() +{ + std::string accuracy_str = + wxGetApp().app_config->get("sla_auto_rotate", "accuracy"); + + std::string method_str = + wxGetApp().app_config->get("sla_auto_rotate", "method_id"); + + if (!accuracy_str.empty()) { + float accuracy = std::stof(accuracy_str); + accuracy = std::max(0.f, std::min(accuracy, 1.f)); + + m_rotoptimizewin_state.accuracy = accuracy; + } + + if (!method_str.empty()) { + int method_id = std::stoi(method_str); + if (method_id < int(RotoptimizeJob::get_methods_count())) + m_rotoptimizewin_state.method_id = method_id; + } +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLGizmoRotate::render_circle(const ColorRGBA& color, bool radius_changed) +#else +void GLGizmoRotate::render_circle() const +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!m_circle.is_initialized() || radius_changed) { + m_circle.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::LineLoop, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(ScaleStepsCount); + init_data.reserve_indices(ScaleStepsCount); + + // vertices + indices + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i * ScaleStepRad); + init_data.add_vertex(Vec3f(::cos(angle) * m_radius, ::sin(angle) * m_radius, 0.0f)); + init_data.add_index(i); + } + + m_circle.init_from(std::move(init_data)); + } + + m_circle.set_color(color); + m_circle.render(); +#else + ::glBegin(GL_LINE_LOOP); + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i) * ScaleStepRad; + const float x = ::cos(angle) * m_radius; + const float y = ::sin(angle) * m_radius; + const float z = 0.0f; + ::glVertex3f((GLfloat)x, (GLfloat)y, (GLfloat)z); + } + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLGizmoRotate::render_scale(const ColorRGBA& color, bool radius_changed) +#else +void GLGizmoRotate::render_scale() const +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ + const float out_radius_long = m_snap_fine_out_radius; + const float out_radius_short = m_radius * (1.0f + 0.5f * ScaleLongTooth); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!m_scale.is_initialized() || radius_changed) { + m_scale.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(2 * ScaleStepsCount); + init_data.reserve_indices(2 * ScaleStepsCount); + + // vertices + indices + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i * ScaleStepRad); + const float cosa = ::cos(angle); + const float sina = ::sin(angle); + const float in_x = cosa * m_radius; + const float in_y = sina * m_radius; + const float out_x = (i % ScaleLongEvery == 0) ? cosa * out_radius_long : cosa * out_radius_short; + const float out_y = (i % ScaleLongEvery == 0) ? sina * out_radius_long : sina * out_radius_short; + + // vertices + init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); + init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); + + // indices + init_data.add_line(i * 2, i * 2 + 1); + } + + m_scale.init_from(std::move(init_data)); + } + + m_scale.set_color(color); + m_scale.render(); +#else + ::glBegin(GL_LINES); + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = (float)i * ScaleStepRad; + const float cosa = ::cos(angle); + const float sina = ::sin(angle); + const float in_x = cosa * m_radius; + const float in_y = sina * m_radius; + const float in_z = 0.0f; + const float out_x = (i % ScaleLongEvery == 0) ? cosa * out_radius_long : cosa * out_radius_short; + const float out_y = (i % ScaleLongEvery == 0) ? sina * out_radius_long : sina * out_radius_short; + const float out_z = 0.0f; + ::glVertex3f((GLfloat)in_x, (GLfloat)in_y, (GLfloat)in_z); + ::glVertex3f((GLfloat)out_x, (GLfloat)out_y, (GLfloat)out_z); + } + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLGizmoRotate::render_snap_radii(const ColorRGBA& color, bool radius_changed) +#else +void GLGizmoRotate::render_snap_radii() const +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ + const float step = 2.0f * float(PI) / float(SnapRegionsCount); + const float in_radius = m_radius / 3.0f; + const float out_radius = 2.0f * in_radius; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!m_snap_radii.is_initialized() || radius_changed) { + m_snap_radii.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(2 * ScaleStepsCount); + init_data.reserve_indices(2 * ScaleStepsCount); + + // vertices + indices + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i * step); + const float cosa = ::cos(angle); + const float sina = ::sin(angle); + const float in_x = cosa * in_radius; + const float in_y = sina * in_radius; + const float out_x = cosa * out_radius; + const float out_y = sina * out_radius; + + // vertices + init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); + init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); + + // indices + init_data.add_line(i * 2, i * 2 + 1); + } + + m_snap_radii.init_from(std::move(init_data)); + } + + m_snap_radii.set_color(color); + m_snap_radii.render(); +#else + ::glBegin(GL_LINES); + for (unsigned int i = 0; i < SnapRegionsCount; ++i) { + const float angle = (float)i * step; + const float cosa = ::cos(angle); + const float sina = ::sin(angle); + const float in_x = cosa * in_radius; + const float in_y = sina * in_radius; + const float in_z = 0.0f; + const float out_x = cosa * out_radius; + const float out_y = sina * out_radius; + const float out_z = 0.0f; + ::glVertex3f((GLfloat)in_x, (GLfloat)in_y, (GLfloat)in_z); + ::glVertex3f((GLfloat)out_x, (GLfloat)out_y, (GLfloat)out_z); + } + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLGizmoRotate::render_reference_radius(const ColorRGBA& color, bool radius_changed) +{ + if (!m_reference_radius.is_initialized() || radius_changed) { + m_reference_radius.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(2); + init_data.reserve_indices(2); + + // vertices + init_data.add_vertex(Vec3f(0.0f, 0.0f, 0.0f)); + init_data.add_vertex(Vec3f(m_radius * (1.0f + GrabberOffset), 0.0f, 0.0f)); + + // indices + init_data.add_line(0, 1); + + m_reference_radius.init_from(std::move(init_data)); + } + + m_reference_radius.set_color(color); + m_reference_radius.render(); +} +#else +void GLGizmoRotate::render_reference_radius() const +{ + ::glBegin(GL_LINES); + ::glVertex3f(0.0f, 0.0f, 0.0f); + ::glVertex3f((GLfloat)(m_radius * (1.0f + GrabberOffset)), 0.0f, 0.0f); + glsafe(::glEnd()); +} +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLGizmoRotate::render_angle_arc(const ColorRGBA& color, bool radius_changed) +#else +void GLGizmoRotate::render_angle() const +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ + const float step_angle = float(m_angle) / float(AngleResolution); + const float ex_radius = m_radius * (1.0f + GrabberOffset); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const bool angle_changed = std::abs(m_old_angle - m_angle) > EPSILON; + m_old_angle = m_angle; + + if (!m_angle_arc.is_initialized() || radius_changed || angle_changed) { + m_angle_arc.reset(); + if (m_angle > 0.0f) { + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::LineStrip, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(1 + AngleResolution); + init_data.reserve_indices(1 + AngleResolution); + + // vertices + indices + for (unsigned int i = 0; i <= AngleResolution; ++i) { + const float angle = float(i) * step_angle; + init_data.add_vertex(Vec3f(::cos(angle) * ex_radius, ::sin(angle) * ex_radius, 0.0f)); + init_data.add_index(i); + } + + m_angle_arc.init_from(std::move(init_data)); + } + } + + m_angle_arc.set_color(color); + m_angle_arc.render(); +#else + ::glBegin(GL_LINE_STRIP); + for (unsigned int i = 0; i <= AngleResolution; ++i) { + const float angle = float(i) * step_angle; + const float x = ::cos(angle) * ex_radius; + const float y = ::sin(angle) * ex_radius; + const float z = 0.0f; + ::glVertex3f((GLfloat)x, (GLfloat)y, (GLfloat)z); + } + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLGizmoRotate::render_grabber_connection(const ColorRGBA& color, bool radius_changed) +{ + if (!m_grabber_connection.model.is_initialized() || radius_changed || !m_grabber_connection.old_center.isApprox(m_grabbers.front().center)) { + m_grabber_connection.model.reset(); + m_grabber_connection.old_center = m_grabbers.front().center; + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(2); + init_data.reserve_indices(2); + + // vertices + init_data.add_vertex(Vec3f(0.0f, 0.0f, 0.0f)); + init_data.add_vertex((Vec3f)m_grabbers.front().center.cast()); + + // indices + init_data.add_line(0, 1); + + m_grabber_connection.model.init_from(std::move(init_data)); + } + + m_grabber_connection.model.set_color(color); + m_grabber_connection.model.render(); +} +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +void GLGizmoRotate::render_grabber(const BoundingBoxf3& box) +{ +#if !ENABLE_LEGACY_OPENGL_REMOVAL + const double grabber_radius = double(m_radius) * (1.0 + double(GrabberOffset)); + m_grabbers[0].center = Vec3d(::cos(m_angle) * grabber_radius, ::sin(m_angle) * grabber_radius, 0.0); + m_grabbers[0].angles.z() = m_angle; + + glsafe(::glColor4fv((m_hover_id != -1) ? m_drag_color.data() : m_highlight_color.data())); + + ::glBegin(GL_LINES); + ::glVertex3f(0.0f, 0.0f, 0.0f); + ::glVertex3dv(m_grabbers[0].center.data()); + glsafe(::glEnd()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + m_grabbers.front().color = m_highlight_color; + render_grabbers(box); +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +Transform3d GLGizmoRotate::local_transform(const Selection& selection) const +{ + Transform3d ret; + + switch (m_axis) + { + case X: + { +#if ENABLE_WORLD_COORDINATE + ret = Geometry::rotation_transform(0.5 * PI * Vec3d::UnitY()) * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()); +#else + ret = Geometry::assemble_transform(Vec3d::Zero(), 0.5 * PI * Vec3d::UnitY()) * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()); +#endif // ENABLE_WORLD_COORDINATE + break; + } + case Y: + { +#if ENABLE_WORLD_COORDINATE + ret = Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()) * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitY()); +#else + ret = Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()) * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitY()); +#endif // ENABLE_WORLD_COORDINATE + break; + } + default: + case Z: + { + ret = Transform3d::Identity(); + break; + } + } + +#if ENABLE_WORLD_COORDINATE + return Geometry::translation_transform(m_center) * m_orient_matrix * ret; +#else + if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) + ret = selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true) * ret; + + return Geometry::assemble_transform(m_center) * ret; +#endif // ENABLE_WORLD_COORDINATE +} +#else +void GLGizmoRotate::transform_to_local(const Selection& selection) const +{ + glsafe(::glTranslated(m_center.x(), m_center.y(), m_center.z())); + +#if ENABLE_WORLD_COORDINATE + glsafe(::glMultMatrixd(m_orient_matrix.data())); +#else + if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) { + const Transform3d orient_matrix = selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true); + glsafe(::glMultMatrixd(orient_matrix.data())); + } +#endif // ENABLE_WORLD_COORDINATE + + switch (m_axis) + { + case X: + { + glsafe(::glRotatef(90.0f, 0.0f, 1.0f, 0.0f)); + glsafe(::glRotatef(-90.0f, 0.0f, 0.0f, 1.0f)); + break; + } + case Y: + { + glsafe(::glRotatef(-90.0f, 0.0f, 0.0f, 1.0f)); + glsafe(::glRotatef(-90.0f, 0.0f, 1.0f, 0.0f)); + break; + } + default: + case Z: + { + // no rotation + break; + } + } +} +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_WORLD_COORDINATE +Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray) const +#else +Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray, const Selection& selection) const +#endif // ENABLE_WORLD_COORDINATE +{ + double half_pi = 0.5 * double(PI); + + Transform3d m = Transform3d::Identity(); + + switch (m_axis) + { + case X: + { + m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); + m.rotate(Eigen::AngleAxisd(-half_pi, Vec3d::UnitY())); + break; + } + case Y: + { + m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitY())); + m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); + break; + } + default: + case Z: + { + // no rotation applied + break; + } + } + +#if ENABLE_WORLD_COORDINATE + m = m * m_orient_matrix.inverse(); +#else + if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) + m = m * selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true).inverse(); +#endif // ENABLE_WORLD_COORDINATE + + m.translate(-m_center); + + const Linef3 local_mouse_ray = transform(mouse_ray, m); + if (std::abs(local_mouse_ray.vector().dot(Vec3d::UnitZ())) < EPSILON) { + // if the ray is parallel to the plane containing the circle + if (std::abs(local_mouse_ray.vector().dot(Vec3d::UnitY())) > 1.0 - EPSILON) + // if the ray is parallel to grabber direction + return Vec3d::UnitX(); + else { + const Vec3d world_pos = (local_mouse_ray.a.x() >= 0.0) ? mouse_ray.a - m_center : mouse_ray.b - m_center; + m.translate(m_center); + return m * world_pos; + } + } + else + return local_mouse_ray.intersect_plane(0.0); +} + +GLGizmoRotate3D::GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoBase(parent, icon_filename, sprite_id) + , m_gizmos({ + GLGizmoRotate(parent, GLGizmoRotate::X), + GLGizmoRotate(parent, GLGizmoRotate::Y), + GLGizmoRotate(parent, GLGizmoRotate::Z) }) +{ + load_rotoptimize_state(); +} + +bool GLGizmoRotate3D::on_mouse(const wxMouseEvent &mouse_event) +{ + if (mouse_event.Dragging() && m_dragging) { + // Apply new temporary rotations +#if ENABLE_WORLD_COORDINATE + TransformationType transformation_type; + if (m_parent.get_selection().is_wipe_tower()) + transformation_type = TransformationType::Instance_Relative_Joint; + else { + switch (wxGetApp().obj_manipul()->get_coordinates_type()) + { + default: + case ECoordinatesType::World: { transformation_type = TransformationType::World_Relative_Joint; break; } + case ECoordinatesType::Instance: { transformation_type = TransformationType::Instance_Relative_Joint; break; } + case ECoordinatesType::Local: { transformation_type = TransformationType::Local_Relative_Joint; break; } + } + } +#else + TransformationType transformation_type(TransformationType::World_Relative_Joint); +#endif // ENABLE_WORLD_COORDINATE + if (mouse_event.AltDown()) + transformation_type.set_independent(); + m_parent.get_selection().rotate(get_rotation(), transformation_type); + } + return use_grabbers(mouse_event); +} + +void GLGizmoRotate3D::data_changed() { + if (m_parent.get_selection().is_wipe_tower()) { +#if !ENABLE_WORLD_COORDINATE + const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const float wipe_tower_rotation_angle = + dynamic_cast( + config.option("wipe_tower_rotation_angle"))->value; + set_rotation(Vec3d(0., 0., (M_PI / 180.) * wipe_tower_rotation_angle)); +#endif // !ENABLE_WORLD_COORDINATE + m_gizmos[0].disable_grabber(); + m_gizmos[1].disable_grabber(); + } + else { +#if !ENABLE_WORLD_COORDINATE + set_rotation(Vec3d::Zero()); +#endif // !ENABLE_WORLD_COORDINATE + m_gizmos[0].enable_grabber(); + m_gizmos[1].enable_grabber(); + } +#if ENABLE_WORLD_COORDINATE + set_rotation(Vec3d::Zero()); +#endif // ENABLE_WORLD_COORDINATE +} + +bool GLGizmoRotate3D::on_init() +{ + for (GLGizmoRotate& g : m_gizmos) + if (!g.init()) return false; + + for (unsigned int i = 0; i < 3; ++i) + m_gizmos[i].set_highlight_color(AXES_COLOR[i]); + + m_shortcut_key = WXK_CONTROL_R; + + return true; +} + +std::string GLGizmoRotate3D::on_get_name() const +{ + return _u8L("Rotate"); +} + +bool GLGizmoRotate3D::on_is_activable() const +{ + const Selection& selection = m_parent.get_selection(); + return !selection.is_any_cut_volume() && !selection.is_any_connector() && !selection.is_empty(); +} + +void GLGizmoRotate3D::on_start_dragging() +{ + assert(0 <= m_hover_id && m_hover_id < 3); + m_gizmos[m_hover_id].start_dragging(); +} + +void GLGizmoRotate3D::on_stop_dragging() +{ + assert(0 <= m_hover_id && m_hover_id < 3); + m_parent.do_rotate(L("Gizmo-Rotate")); + m_gizmos[m_hover_id].stop_dragging(); +} + +void GLGizmoRotate3D::on_dragging(const UpdateData &data) +{ + assert(0 <= m_hover_id && m_hover_id < 3); + m_gizmos[m_hover_id].dragging(data); +} + +void GLGizmoRotate3D::on_render() +{ + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + + if (m_hover_id == -1 || m_hover_id == 0) + m_gizmos[X].render(); + + if (m_hover_id == -1 || m_hover_id == 1) + m_gizmos[Y].render(); + + if (m_hover_id == -1 || m_hover_id == 2) + m_gizmos[Z].render(); +} + +#if ENABLE_RAYCAST_PICKING +void GLGizmoRotate3D::on_register_raycasters_for_picking() +{ + // the gizmo grabbers are rendered on top of the scene, so the raytraced picker should take it into account + m_parent.set_raycaster_gizmos_on_top(true); + for (GLGizmoRotate& g : m_gizmos) { + g.register_raycasters_for_picking(); + } +} + +void GLGizmoRotate3D::on_unregister_raycasters_for_picking() +{ + for (GLGizmoRotate& g : m_gizmos) { + g.unregister_raycasters_for_picking(); + } + m_parent.set_raycaster_gizmos_on_top(false); +} +#endif // ENABLE_RAYCAST_PICKING + +GLGizmoRotate3D::RotoptimzeWindow::RotoptimzeWindow(ImGuiWrapper * imgui, + State & state, + const Alignment &alignment) + : m_imgui{imgui} +{ + imgui->begin(_L("Optimize orientation"), ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoCollapse); + + // adjust window position to avoid overlap the view toolbar + float win_h = ImGui::GetWindowHeight(); + float x = alignment.x, y = alignment.y; + y = std::min(y, alignment.bottom_limit - win_h); + ImGui::SetWindowPos(ImVec2(x, y), ImGuiCond_Always); + + float max_text_w = 0.; + auto padding = ImGui::GetStyle().FramePadding; + padding.x *= 2.f; + padding.y *= 2.f; + + for (size_t i = 0; i < RotoptimizeJob::get_methods_count(); ++i) { + float w = + ImGui::CalcTextSize(RotoptimizeJob::get_method_name(i).c_str()).x + + padding.x + ImGui::GetFrameHeight(); + max_text_w = std::max(w, max_text_w); + } + + ImGui::PushItemWidth(max_text_w); + + if (ImGui::BeginCombo("", RotoptimizeJob::get_method_name(state.method_id).c_str())) { + for (size_t i = 0; i < RotoptimizeJob::get_methods_count(); ++i) { + if (ImGui::Selectable(RotoptimizeJob::get_method_name(i).c_str())) { + state.method_id = i; + wxGetApp().app_config->set("sla_auto_rotate", + "method_id", + std::to_string(state.method_id)); + } + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", RotoptimizeJob::get_method_description(i).c_str()); + } + + ImGui::EndCombo(); + } + + ImVec2 sz = ImGui::GetItemRectSize(); + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", RotoptimizeJob::get_method_description(state.method_id).c_str()); + + ImGui::Separator(); + + auto btn_txt = _L("Apply"); + auto btn_txt_sz = ImGui::CalcTextSize(btn_txt.c_str()); + ImVec2 button_sz = {btn_txt_sz.x + padding.x, btn_txt_sz.y + padding.y}; + ImGui::SetCursorPosX(padding.x + sz.x - button_sz.x); + + if (!wxGetApp().plater()->get_ui_job_worker().is_idle()) + imgui->disabled_begin(true); + + if ( imgui->button(btn_txt) ) { + replace_job(wxGetApp().plater()->get_ui_job_worker(), + std::make_unique()); + } + + imgui->disabled_end(); +} + +GLGizmoRotate3D::RotoptimzeWindow::~RotoptimzeWindow() +{ + m_imgui->end(); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp index 68a764359..54a40dd59 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp @@ -1,233 +1,233 @@ -#ifndef slic3r_GLGizmoRotate_hpp_ -#define slic3r_GLGizmoRotate_hpp_ - -#include "GLGizmoBase.hpp" - -namespace Slic3r { -namespace GUI { -class Selection; - -class GLGizmoRotate : public GLGizmoBase -{ - static const float Offset; - static const unsigned int AngleResolution; - static const unsigned int ScaleStepsCount; - static const float ScaleStepRad; - static const unsigned int ScaleLongEvery; - static const float ScaleLongTooth; - static const unsigned int SnapRegionsCount; - static const float GrabberOffset; - -public: - enum Axis : unsigned char - { - X=0, - Y=1, - Z=2 - }; - -private: - Axis m_axis; - double m_angle{ 0.0 }; - Vec3d m_center{ Vec3d::Zero() }; - float m_radius{ 0.0f }; - float m_snap_coarse_in_radius{ 0.0f }; - float m_snap_coarse_out_radius{ 0.0f }; - float m_snap_fine_in_radius{ 0.0f }; - float m_snap_fine_out_radius{ 0.0f }; -#if ENABLE_WORLD_COORDINATE - BoundingBoxf3 m_bounding_box; - Transform3d m_orient_matrix{ Transform3d::Identity() }; -#endif // ENABLE_WORLD_COORDINATE - -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLModel m_circle; - GLModel m_scale; - GLModel m_snap_radii; - GLModel m_reference_radius; - GLModel m_angle_arc; - struct GrabberConnection - { - GLModel model; - Vec3d old_center{ Vec3d::Zero() }; - }; - GrabberConnection m_grabber_connection; - float m_old_radius{ 0.0f }; - float m_old_hover_radius{ 0.0f }; - float m_old_angle{ 0.0f }; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - ColorRGBA m_drag_color; - ColorRGBA m_highlight_color; - -public: - GLGizmoRotate(GLCanvas3D& parent, Axis axis); - virtual ~GLGizmoRotate() = default; - - double get_angle() const { return m_angle; } - void set_angle(double angle); - - std::string get_tooltip() const override; - - void start_dragging(); - void stop_dragging(); - - void enable_grabber(); - void disable_grabber(); - - void set_highlight_color(const ColorRGBA &color); - - /// - /// Postpone to Grabber for move - /// Detect move of object by dragging - /// - /// Keep information about mouse click - /// Return True when use the information otherwise False. - bool on_mouse(const wxMouseEvent &mouse_event) override; - void dragging(const UpdateData &data); - -protected: - bool on_init() override; - std::string on_get_name() const override { return ""; } - void on_start_dragging() override; - void on_dragging(const UpdateData &data) override; - void on_render() override; -#if !ENABLE_RAYCAST_PICKING - void on_render_for_picking() override; -#endif // !ENABLE_RAYCAST_PICKING - -private: -#if ENABLE_LEGACY_OPENGL_REMOVAL - void render_circle(const ColorRGBA& color, bool radius_changed); - void render_scale(const ColorRGBA& color, bool radius_changed); - void render_snap_radii(const ColorRGBA& color, bool radius_changed); - void render_reference_radius(const ColorRGBA& color, bool radius_changed); - void render_angle_arc(const ColorRGBA& color, bool radius_changed); - void render_grabber_connection(const ColorRGBA& color, bool radius_changed); -#else - void render_circle() const; - void render_scale() const; - void render_snap_radii() const; - void render_reference_radius() const; - void render_angle() const; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - void render_grabber(const BoundingBoxf3& box); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - Transform3d local_transform(const Selection& selection) const; -#else - void transform_to_local(const Selection& selection) const; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - // returns the intersection of the mouse ray with the plane perpendicular to the gizmo axis, in local coordinate -#if ENABLE_WORLD_COORDINATE - Vec3d mouse_position_in_local_plane(const Linef3& mouse_ray) const; -#else - Vec3d mouse_position_in_local_plane(const Linef3& mouse_ray, const Selection& selection) const; -#endif // ENABLE_WORLD_COORDINATE - -#if ENABLE_WORLD_COORDINATE - void init_data_from_selection(const Selection& selection); -#endif // ENABLE_WORLD_COORDINATE -}; - -class GLGizmoRotate3D : public GLGizmoBase -{ - std::array m_gizmos; - -public: - GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); - - Vec3d get_rotation() const { return Vec3d(m_gizmos[X].get_angle(), m_gizmos[Y].get_angle(), m_gizmos[Z].get_angle()); } - void set_rotation(const Vec3d& rotation) { m_gizmos[X].set_angle(rotation.x()); m_gizmos[Y].set_angle(rotation.y()); m_gizmos[Z].set_angle(rotation.z()); } - - std::string get_tooltip() const override { - std::string tooltip = m_gizmos[X].get_tooltip(); - if (tooltip.empty()) - tooltip = m_gizmos[Y].get_tooltip(); - if (tooltip.empty()) - tooltip = m_gizmos[Z].get_tooltip(); - return tooltip; - } - - /// - /// Postpone to Rotation - /// - /// Keep information about mouse click - /// Return True when use the information otherwise False. - bool on_mouse(const wxMouseEvent &mouse_event) override; - - void data_changed() override; -protected: - bool on_init() override; - std::string on_get_name() const override; - void on_set_state() override { - for (GLGizmoRotate& g : m_gizmos) - g.set_state(m_state); - } - void on_set_hover_id() override { - for (int i = 0; i < 3; ++i) - m_gizmos[i].set_hover_id((m_hover_id == i) ? 0 : -1); - } - void on_enable_grabber(unsigned int id) override { - if (id < 3) - m_gizmos[id].enable_grabber(); - } - void on_disable_grabber(unsigned int id) override { - if (id < 3) - m_gizmos[id].disable_grabber(); - } - bool on_is_activable() const override; - void on_start_dragging() override; - void on_stop_dragging() override; - void on_dragging(const UpdateData &data) override; - - void on_render() override; -#if ENABLE_RAYCAST_PICKING - virtual void on_register_raycasters_for_picking() override; - virtual void on_unregister_raycasters_for_picking() override; -#else - void on_render_for_picking() override { - for (GLGizmoRotate& g : m_gizmos) { - g.render_for_picking(); - } - } -#endif // ENABLE_RAYCAST_PICKING - - void on_render_input_window(float x, float y, float bottom_limit) override; - -private: - - class RotoptimzeWindow - { - ImGuiWrapper *m_imgui = nullptr; - - public: - struct State { - float accuracy = 1.f; - int method_id = 0; - }; - - struct Alignment { float x, y, bottom_limit; }; - - RotoptimzeWindow(ImGuiWrapper * imgui, - State & state, - const Alignment &bottom_limit); - - ~RotoptimzeWindow(); - - RotoptimzeWindow(const RotoptimzeWindow&) = delete; - RotoptimzeWindow(RotoptimzeWindow &&) = delete; - RotoptimzeWindow& operator=(const RotoptimzeWindow &) = delete; - RotoptimzeWindow& operator=(RotoptimzeWindow &&) = delete; - }; - - RotoptimzeWindow::State m_rotoptimizewin_state = {}; - - void load_rotoptimize_state(); -}; - -} // namespace GUI -} // namespace Slic3r - -#endif // slic3r_GLGizmoRotate_hpp_ +#ifndef slic3r_GLGizmoRotate_hpp_ +#define slic3r_GLGizmoRotate_hpp_ + +#include "GLGizmoBase.hpp" + +namespace Slic3r { +namespace GUI { +class Selection; + +class GLGizmoRotate : public GLGizmoBase +{ + static const float Offset; + static const unsigned int AngleResolution; + static const unsigned int ScaleStepsCount; + static const float ScaleStepRad; + static const unsigned int ScaleLongEvery; + static const float ScaleLongTooth; + static const unsigned int SnapRegionsCount; + static const float GrabberOffset; + +public: + enum Axis : unsigned char + { + X=0, + Y=1, + Z=2 + }; + +private: + Axis m_axis; + double m_angle{ 0.0 }; + Vec3d m_center{ Vec3d::Zero() }; + float m_radius{ 0.0f }; + float m_snap_coarse_in_radius{ 0.0f }; + float m_snap_coarse_out_radius{ 0.0f }; + float m_snap_fine_in_radius{ 0.0f }; + float m_snap_fine_out_radius{ 0.0f }; +#if ENABLE_WORLD_COORDINATE + BoundingBoxf3 m_bounding_box; + Transform3d m_orient_matrix{ Transform3d::Identity() }; +#endif // ENABLE_WORLD_COORDINATE + +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLModel m_circle; + GLModel m_scale; + GLModel m_snap_radii; + GLModel m_reference_radius; + GLModel m_angle_arc; + struct GrabberConnection + { + GLModel model; + Vec3d old_center{ Vec3d::Zero() }; + }; + GrabberConnection m_grabber_connection; + float m_old_radius{ 0.0f }; + float m_old_hover_radius{ 0.0f }; + float m_old_angle{ 0.0f }; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + ColorRGBA m_drag_color; + ColorRGBA m_highlight_color; + +public: + GLGizmoRotate(GLCanvas3D& parent, Axis axis); + virtual ~GLGizmoRotate() = default; + + double get_angle() const { return m_angle; } + void set_angle(double angle); + + std::string get_tooltip() const override; + + void start_dragging(); + void stop_dragging(); + + void enable_grabber(); + void disable_grabber(); + + void set_highlight_color(const ColorRGBA &color); + + /// + /// Postpone to Grabber for move + /// Detect move of object by dragging + /// + /// Keep information about mouse click + /// Return True when use the information otherwise False. + bool on_mouse(const wxMouseEvent &mouse_event) override; + void dragging(const UpdateData &data); + +protected: + bool on_init() override; + std::string on_get_name() const override { return ""; } + void on_start_dragging() override; + void on_dragging(const UpdateData &data) override; + void on_render() override; +#if !ENABLE_RAYCAST_PICKING + void on_render_for_picking() override; +#endif // !ENABLE_RAYCAST_PICKING + +private: +#if ENABLE_LEGACY_OPENGL_REMOVAL + void render_circle(const ColorRGBA& color, bool radius_changed); + void render_scale(const ColorRGBA& color, bool radius_changed); + void render_snap_radii(const ColorRGBA& color, bool radius_changed); + void render_reference_radius(const ColorRGBA& color, bool radius_changed); + void render_angle_arc(const ColorRGBA& color, bool radius_changed); + void render_grabber_connection(const ColorRGBA& color, bool radius_changed); +#else + void render_circle() const; + void render_scale() const; + void render_snap_radii() const; + void render_reference_radius() const; + void render_angle() const; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + void render_grabber(const BoundingBoxf3& box); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + Transform3d local_transform(const Selection& selection) const; +#else + void transform_to_local(const Selection& selection) const; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + // returns the intersection of the mouse ray with the plane perpendicular to the gizmo axis, in local coordinate +#if ENABLE_WORLD_COORDINATE + Vec3d mouse_position_in_local_plane(const Linef3& mouse_ray) const; +#else + Vec3d mouse_position_in_local_plane(const Linef3& mouse_ray, const Selection& selection) const; +#endif // ENABLE_WORLD_COORDINATE + +#if ENABLE_WORLD_COORDINATE + void init_data_from_selection(const Selection& selection); +#endif // ENABLE_WORLD_COORDINATE +}; + +class GLGizmoRotate3D : public GLGizmoBase +{ + std::array m_gizmos; + +public: + GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + + Vec3d get_rotation() const { return Vec3d(m_gizmos[X].get_angle(), m_gizmos[Y].get_angle(), m_gizmos[Z].get_angle()); } + void set_rotation(const Vec3d& rotation) { m_gizmos[X].set_angle(rotation.x()); m_gizmos[Y].set_angle(rotation.y()); m_gizmos[Z].set_angle(rotation.z()); } + + std::string get_tooltip() const override { + std::string tooltip = m_gizmos[X].get_tooltip(); + if (tooltip.empty()) + tooltip = m_gizmos[Y].get_tooltip(); + if (tooltip.empty()) + tooltip = m_gizmos[Z].get_tooltip(); + return tooltip; + } + + /// + /// Postpone to Rotation + /// + /// Keep information about mouse click + /// Return True when use the information otherwise False. + bool on_mouse(const wxMouseEvent &mouse_event) override; + + void data_changed() override; +protected: + bool on_init() override; + std::string on_get_name() const override; + void on_set_state() override { + for (GLGizmoRotate& g : m_gizmos) + g.set_state(m_state); + } + void on_set_hover_id() override { + for (int i = 0; i < 3; ++i) + m_gizmos[i].set_hover_id((m_hover_id == i) ? 0 : -1); + } + void on_enable_grabber(unsigned int id) override { + if (id < 3) + m_gizmos[id].enable_grabber(); + } + void on_disable_grabber(unsigned int id) override { + if (id < 3) + m_gizmos[id].disable_grabber(); + } + bool on_is_activable() const override; + void on_start_dragging() override; + void on_stop_dragging() override; + void on_dragging(const UpdateData &data) override; + + void on_render() override; +#if ENABLE_RAYCAST_PICKING + virtual void on_register_raycasters_for_picking() override; + virtual void on_unregister_raycasters_for_picking() override; +#else + void on_render_for_picking() override { + for (GLGizmoRotate& g : m_gizmos) { + g.render_for_picking(); + } + } +#endif // ENABLE_RAYCAST_PICKING + + void on_render_input_window(float x, float y, float bottom_limit) override; + +private: + + class RotoptimzeWindow + { + ImGuiWrapper *m_imgui = nullptr; + + public: + struct State { + float accuracy = 1.f; + int method_id = 0; + }; + + struct Alignment { float x, y, bottom_limit; }; + + RotoptimzeWindow(ImGuiWrapper * imgui, + State & state, + const Alignment &bottom_limit); + + ~RotoptimzeWindow(); + + RotoptimzeWindow(const RotoptimzeWindow&) = delete; + RotoptimzeWindow(RotoptimzeWindow &&) = delete; + RotoptimzeWindow& operator=(const RotoptimzeWindow &) = delete; + RotoptimzeWindow& operator=(RotoptimzeWindow &&) = delete; + }; + + RotoptimzeWindow::State m_rotoptimizewin_state = {}; + + void load_rotoptimize_state(); +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GLGizmoRotate_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp index 572e011e5..9b49cf0b4 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp @@ -8,6 +8,7 @@ #if ENABLE_LEGACY_OPENGL_REMOVAL #include "slic3r/GUI/Plater.hpp" #endif // ENABLE_LEGACY_OPENGL_REMOVAL +#include "libslic3r/Model.hpp" #include @@ -103,6 +104,12 @@ bool GLGizmoScale3D::on_mouse(const wxMouseEvent &mouse_event) return use_grabbers(mouse_event); } +void GLGizmoScale3D::enable_ununiversal_scale(bool enable) +{ + for (unsigned int i = 0; i < 6; ++i) + m_grabbers[i].enabled = enable; +} + void GLGizmoScale3D::data_changed() { #if ENABLE_WORLD_COORDINATE @@ -161,7 +168,7 @@ std::string GLGizmoScale3D::on_get_name() const bool GLGizmoScale3D::on_is_activable() const { const Selection& selection = m_parent.get_selection(); - return !selection.is_empty() && !selection.is_wipe_tower(); + return !selection.is_any_cut_volume() && !selection.is_any_connector() && !selection.is_empty() && !selection.is_wipe_tower(); } void GLGizmoScale3D::on_start_dragging() @@ -448,7 +455,7 @@ void GLGizmoScale3D::on_render() // draw grabbers render_grabbers(grabber_mean_size); } - else if (m_hover_id == 0 || m_hover_id == 1) { + else if ((m_hover_id == 0 || m_hover_id == 1) && m_grabbers[0].enabled && m_grabbers[1].enabled) { #if ENABLE_LEGACY_OPENGL_REMOVAL // draw connections #if ENABLE_GL_CORE_PROFILE @@ -493,7 +500,7 @@ void GLGizmoScale3D::on_render() shader->stop_using(); } } - else if (m_hover_id == 2 || m_hover_id == 3) { + else if ((m_hover_id == 2 || m_hover_id == 3) && m_grabbers[2].enabled && m_grabbers[3].enabled) { #if ENABLE_LEGACY_OPENGL_REMOVAL // draw connections #if ENABLE_GL_CORE_PROFILE @@ -538,7 +545,7 @@ void GLGizmoScale3D::on_render() shader->stop_using(); } } - else if (m_hover_id == 4 || m_hover_id == 5) { + else if ((m_hover_id == 4 || m_hover_id == 5) && m_grabbers[4].enabled && m_grabbers[5].enabled) { #if ENABLE_LEGACY_OPENGL_REMOVAL // draw connections #if ENABLE_GL_CORE_PROFILE diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp index b66da89bc..b92c54f18 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp @@ -85,6 +85,7 @@ public: bool on_mouse(const wxMouseEvent &mouse_event) override; void data_changed() override; + void enable_ununiversal_scale(bool enable); protected: virtual bool on_init() override; virtual std::string on_get_name() const override; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp index 418f180de..a49356579 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp @@ -156,7 +156,7 @@ void GLGizmoSeam::on_render_input_window(float x, float y, float bottom_limit) else { if (m_imgui->button(m_desc.at("reset_direction"))) { wxGetApp().CallAfter([this](){ - m_c->object_clipper()->set_position(-1., false); + m_c->object_clipper()->set_position_by_ratio(-1., false); }); } } @@ -165,7 +165,7 @@ void GLGizmoSeam::on_render_input_window(float x, float y, float bottom_limit) ImGui::SameLine(sliders_left_width); ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true, _L("Ctrl + Mouse wheel"))) - m_c->object_clipper()->set_position(clp_dist, true); + m_c->object_clipper()->set_position_by_ratio(clp_dist, true); ImGui::Separator(); if (m_imgui->button(m_desc.at("remove_all"))) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index b562eb49a..579cbbe67 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -673,19 +673,19 @@ bool GLGizmoSlaSupports::gizmo_event(SLAGizmoEventType action, const Vec2d& mous if (action == SLAGizmoEventType::MouseWheelUp && control_down) { double pos = m_c->object_clipper()->get_position(); pos = std::min(1., pos + 0.01); - m_c->object_clipper()->set_position(pos, true); + m_c->object_clipper()->set_position_by_ratio(pos, true); return true; } if (action == SLAGizmoEventType::MouseWheelDown && control_down) { double pos = m_c->object_clipper()->get_position(); pos = std::max(0., pos - 0.01); - m_c->object_clipper()->set_position(pos, true); + m_c->object_clipper()->set_position_by_ratio(pos, true); return true; } if (action == SLAGizmoEventType::ResetClippingPlane) { - m_c->object_clipper()->set_position(-1., false); + m_c->object_clipper()->set_position_by_ratio(-1., false); return true; } @@ -972,7 +972,7 @@ RENDER_AGAIN: else { if (m_imgui->button(m_desc.at("reset_direction"))) { wxGetApp().CallAfter([this](){ - m_c->object_clipper()->set_position(-1., false); + m_c->object_clipper()->set_position_by_ratio(-1., false); }); } } @@ -981,7 +981,7 @@ RENDER_AGAIN: ImGui::PushItemWidth(window_width - clipping_slider_left); float clp_dist = m_c->object_clipper()->get_position(); if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f")) - m_c->object_clipper()->set_position(clp_dist, true); + m_c->object_clipper()->set_position_by_ratio(clp_dist, true); if (m_imgui->button("?")) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp index ed48b6e5e..136276803 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp @@ -62,8 +62,8 @@ public: void delete_selected_points(bool force = false); //ClippingPlane get_sla_clipping_plane() const; - bool is_in_editing_mode() const { return m_editing_mode; } - bool is_selection_rectangle_dragging() const { return m_selection_rectangle.is_dragging(); } + bool is_in_editing_mode() const override { return m_editing_mode; } + bool is_selection_rectangle_dragging() const override { return m_selection_rectangle.is_dragging(); } bool has_backend_supports() const; void reslice_SLA_supports(bool postpone_error_messages = false) const; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp index ce3ba1d4a..9734f1b77 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp @@ -425,8 +425,11 @@ void ObjectClipper::render_cut() const if (m_clp_ratio == 0.) return; const SelectionInfo* sel_info = get_pool()->selection_info(); + int sel_instance_idx = sel_info->get_active_instance(); + if (sel_instance_idx < 0) + return; const ModelObject* mo = sel_info->model_object(); - const Geometry::Transformation inst_trafo = mo->instances[sel_info->get_active_instance()]->get_transformation(); + const Geometry::Transformation inst_trafo = mo->instances[sel_instance_idx]->get_transformation(); size_t clipper_id = 0; for (const ModelVolume* mv : mo->volumes) { @@ -440,19 +443,30 @@ void ObjectClipper::render_cut() const clipper->set_limiting_plane(ClippingPlane(Vec3d::UnitZ(), -SINKING_Z_THRESHOLD)); #if ENABLE_LEGACY_OPENGL_REMOVAL clipper->render_cut({ 1.0f, 0.37f, 0.0f, 1.0f }); + clipper->render_contour({ 1.f, 1.f, 1.f, 1.f}); #else glsafe(::glPushMatrix()); glsafe(::glColor3f(1.0f, 0.37f, 0.0f)); clipper->render_cut(); - glsafe(::glPopMatrix()); + glsafe(::glColor3f(1.f, 1.f, 1.f)); + clipper->render_contour(); #endif // ENABLE_LEGACY_OPENGL_REMOVAL ++clipper_id; } } +bool ObjectClipper::is_projection_inside_cut(const Vec3d& point) const +{ + return m_clp_ratio != 0. && std::any_of(m_clippers.begin(), m_clippers.end(), [point](const std::unique_ptr& cl) { return cl->is_projection_inside_cut(point); }); +} -void ObjectClipper::set_position(double pos, bool keep_normal) +bool ObjectClipper::has_valid_contour() const +{ + return m_clp_ratio != 0. && std::any_of(m_clippers.begin(), m_clippers.end(), [](const std::unique_ptr& cl) { return cl->has_valid_contour(); }); +} + +void ObjectClipper::set_position_by_ratio(double pos, bool keep_normal) { const ModelObject* mo = get_pool()->selection_info()->model_object(); int active_inst = get_pool()->selection_info()->get_active_instance(); @@ -470,7 +484,36 @@ void ObjectClipper::set_position(double pos, bool keep_normal) get_pool()->get_canvas()->set_as_dirty(); } +void ObjectClipper::set_range_and_pos(const Vec3d& cpl_normal, double cpl_offset, double pos) +{ + m_clp.reset(new ClippingPlane(cpl_normal, cpl_offset)); + m_clp_ratio = pos; + get_pool()->get_canvas()->set_as_dirty(); +} +const ClippingPlane* ObjectClipper::get_clipping_plane(bool ignore_hide_clipped) const +{ + static const ClippingPlane no_clip = ClippingPlane::ClipsNothing(); + return (ignore_hide_clipped || m_hide_clipped) ? m_clp.get() : &no_clip; +} + +void ObjectClipper::set_behavior(bool hide_clipped, bool fill_cut, double contour_width) +{ + m_hide_clipped = hide_clipped; + for (auto& clipper : m_clippers) + clipper->set_behaviour(fill_cut, contour_width); +} + +void ObjectClipper::pass_mouse_click(const Vec3d& pt) +{ + for (auto& clipper : m_clippers) + clipper->pass_mouse_click(pt); +} + +std::vector ObjectClipper::get_disabled_contours() const +{ + return std::vector(); +} void SupportsClipper::on_update() { @@ -557,11 +600,13 @@ void SupportsClipper::render_cut() const #if ENABLE_LEGACY_OPENGL_REMOVAL m_clipper->render_cut({ 1.0f, 0.f, 0.37f, 1.0f }); + m_clipper->render_contour({ 1.f, 1.f, 1.f, 1.f }); #else glsafe(::glPushMatrix()); glsafe(::glColor3f(1.0f, 0.f, 0.37f)); m_clipper->render_cut(); - glsafe(::glPopMatrix()); + glsafe(::glColor3f(1.0f, 1.f, 1.f)); + m_clipper->render_contour(); #endif // ENABLE_LEGACY_OPENGL_REMOVAL } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp index 7dd2c110e..f8ead27f9 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp @@ -255,10 +255,19 @@ public: CommonGizmosDataID get_dependencies() const override { return CommonGizmosDataID::SelectionInfo; } #endif // NDEBUG - void set_position(double pos, bool keep_normal); + void set_normal(const Vec3d& dir); double get_position() const { return m_clp_ratio; } - ClippingPlane* get_clipping_plane() const { return m_clp.get(); } + const ClippingPlane* get_clipping_plane(bool ignore_hide_clipped = false) const; void render_cut() const; + void set_position_by_ratio(double pos, bool keep_normal); + void set_range_and_pos(const Vec3d& cpl_normal, double cpl_offset, double pos); + void set_behavior(bool hide_clipped, bool fill_cut, double contour_width); + + void pass_mouse_click(const Vec3d& pt); + std::vector get_disabled_contours() const; + + bool is_projection_inside_cut(const Vec3d& point_in) const; + bool has_valid_contour() const; protected: @@ -271,6 +280,7 @@ private: std::unique_ptr m_clp; double m_clp_ratio = 0.; double m_active_inst_bb_radius = 0.; + bool m_hide_clipped = true; }; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 8759e880d..add1eb94c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -99,7 +99,7 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoScale3D(m_parent, "scale.svg", 1)); m_gizmos.emplace_back(new GLGizmoRotate3D(m_parent, "rotate.svg", 2)); m_gizmos.emplace_back(new GLGizmoFlatten(m_parent, "place.svg", 3)); - m_gizmos.emplace_back(new GLGizmoCut(m_parent, "cut.svg", 4)); + m_gizmos.emplace_back(new GLGizmoCut3D(m_parent, "cut.svg", 4)); m_gizmos.emplace_back(new GLGizmoHollow(m_parent, "hollow.svg", 5)); m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", 6)); m_gizmos.emplace_back(new GLGizmoFdmSupports(m_parent, "fdm_supports.svg", 7)); @@ -288,6 +288,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p return dynamic_cast(m_gizmos[Seam].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == MmuSegmentation) return dynamic_cast(m_gizmos[MmuSegmentation].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); + else if (m_current == Cut) + return dynamic_cast(m_gizmos[Cut].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else return false; } @@ -505,7 +507,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) #endif /* __APPLE__ */ { // Sla gizmo selects all support points - if ((m_current == SlaSupports || m_current == Hollow) && gizmo_event(SLAGizmoEventType::SelectAll)) + if ((m_current == SlaSupports || m_current == Hollow || m_current == Cut) && gizmo_event(SLAGizmoEventType::SelectAll)) processed = true; break; @@ -549,7 +551,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) case WXK_BACK: case WXK_DELETE: { - if ((m_current == SlaSupports || m_current == Hollow) && gizmo_event(SLAGizmoEventType::Delete)) + if ((m_current == SlaSupports || m_current == Hollow || m_current == Cut) && gizmo_event(SLAGizmoEventType::Delete)) processed = true; break; @@ -608,20 +610,11 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) if (evt.GetEventType() == wxEVT_KEY_UP) { - if (m_current == SlaSupports || m_current == Hollow) + if (m_current == SlaSupports || m_current == Hollow || m_current == Cut) { - bool is_editing = true; - bool is_rectangle_dragging = false; - - if (m_current == SlaSupports) { - GLGizmoSlaSupports* gizmo = dynamic_cast(get_current()); - is_editing = gizmo->is_in_editing_mode(); - is_rectangle_dragging = gizmo->is_selection_rectangle_dragging(); - } - else { - GLGizmoHollow* gizmo = dynamic_cast(get_current()); - is_rectangle_dragging = gizmo->is_selection_rectangle_dragging(); - } + GLGizmoBase* gizmo = get_current(); + const bool is_editing = m_current == Hollow ? true : gizmo->is_in_editing_mode(); + const bool is_rectangle_dragging = gizmo->is_selection_rectangle_dragging(); if (keyCode == WXK_SHIFT) { @@ -643,7 +636,7 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) else if (evt.GetEventType() == wxEVT_KEY_DOWN) { if ((m_current == SlaSupports) && ((keyCode == WXK_SHIFT) || (keyCode == WXK_ALT)) - && dynamic_cast(get_current())->is_in_editing_mode()) + && get_current()->is_in_editing_mode()) { // m_parent.set_cursor(GLCanvas3D::Cross); processed = true; @@ -651,8 +644,8 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) else if (m_current == Cut) { auto do_move = [this, &processed](double delta_z) { - GLGizmoCut* cut = dynamic_cast(get_current()); - cut->set_cut_z(delta_z + cut->get_cut_z()); + GLGizmoCut3D* cut = dynamic_cast(get_current()); + cut->shift_cut_z(delta_z); processed = true; }; @@ -660,6 +653,9 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) { case WXK_NUMPAD_UP: case WXK_UP: { do_move(1.0); break; } case WXK_NUMPAD_DOWN: case WXK_DOWN: { do_move(-1.0); break; } + case WXK_SHIFT : case WXK_ALT: { + processed = get_current()->is_in_editing_mode(); + } default: { break; } } } else if (m_current == Simplify && keyCode == WXK_ESCAPE) { @@ -1042,6 +1038,11 @@ GLGizmoBase* GLGizmosManager::get_current() const return ((m_current == Undefined) || m_gizmos.empty()) ? nullptr : m_gizmos[m_current].get(); } +GLGizmoBase* GLGizmosManager::get_gizmo(GLGizmosManager::EType type) const +{ + return ((type == Undefined) || m_gizmos.empty()) ? nullptr : m_gizmos[type].get(); +} + GLGizmosManager::EType GLGizmosManager::get_gizmo_from_name(const std::string& gizmo_name) const { std::vector selectable_idxs = get_selectable_idxs(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 8a708f62a..61de06280 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -199,6 +199,7 @@ public: EType get_current_type() const { return m_current; } GLGizmoBase* get_current() const; + GLGizmoBase* get_gizmo(GLGizmosManager::EType type) const; EType get_gizmo_from_name(const std::string& gizmo_name) const; bool is_running() const; diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index fe3f69a58..3bdc80abd 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -56,6 +56,11 @@ static const std::map font_icons = { {ImGui::PreferencesHoverButton, "notification_preferences_hover"}, {ImGui::SliderFloatEditBtnIcon, "edit_button" }, {ImGui::SliderFloatEditBtnPressedIcon, "edit_button_pressed" }, + {ImGui::ExpandBtn , "expand_btn" }, + {ImGui::CollapseBtn , "collapse_btn" }, + {ImGui::RevertButton , "undo" }, + {ImGui::WarningMarkerSmall , "notification_warning" }, + {ImGui::InfoMarkerSmall , "notification_info" }, }; static const std::map font_icons_large = { diff --git a/src/slic3r/GUI/MeshUtils.cpp b/src/slic3r/GUI/MeshUtils.cpp index 960948b24..e33d29ba1 100644 --- a/src/slic3r/GUI/MeshUtils.cpp +++ b/src/slic3r/GUI/MeshUtils.cpp @@ -16,15 +16,27 @@ #include +#include + namespace Slic3r { namespace GUI { +void MeshClipper::set_behaviour(bool fill_cut, double contour_width) +{ + if (fill_cut != m_fill_cut || contour_width != m_contour_width) + m_result.reset(); + m_fill_cut = fill_cut; + m_contour_width = contour_width; +} + + + void MeshClipper::set_plane(const ClippingPlane& plane) { if (m_plane != plane) { m_plane = plane; - m_triangles_valid = false; + m_result.reset(); } } @@ -33,7 +45,7 @@ void MeshClipper::set_limiting_plane(const ClippingPlane& plane) { if (m_limiting_plane != plane) { m_limiting_plane = plane; - m_triangles_valid = false; + m_result.reset(); } } @@ -43,8 +55,7 @@ void MeshClipper::set_mesh(const TriangleMesh& mesh) { if (m_mesh != &mesh) { m_mesh = &mesh; - m_triangles_valid = false; - m_triangles2d.resize(0); + m_result.reset(); } } @@ -52,8 +63,7 @@ void MeshClipper::set_negative_mesh(const TriangleMesh& mesh) { if (m_negative_mesh != &mesh) { m_negative_mesh = &mesh; - m_triangles_valid = false; - m_triangles2d.resize(0); + m_result.reset(); } } @@ -63,8 +73,7 @@ void MeshClipper::set_transformation(const Geometry::Transformation& trafo) { if (! m_trafo.get_matrix().isApprox(trafo.get_matrix())) { m_trafo = trafo; - m_triangles_valid = false; - m_triangles2d.resize(0); + m_result.reset(); } } @@ -74,13 +83,9 @@ void MeshClipper::render_cut(const ColorRGBA& color) void MeshClipper::render_cut() #endif // ENABLE_LEGACY_OPENGL_REMOVAL { - if (! m_triangles_valid) + if (! m_result) recalculate_triangles(); - #if ENABLE_LEGACY_OPENGL_REMOVAL - if (m_model.vertices_count() == 0 || m_model.indices_count() == 0) - return; - GLShaderProgram* curr_shader = wxGetApp().get_current_shader(); if (curr_shader != nullptr) curr_shader->stop_using(); @@ -91,8 +96,10 @@ void MeshClipper::render_cut() const Camera& camera = wxGetApp().plater()->get_camera(); shader->set_uniform("view_model_matrix", camera.get_view_matrix()); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - m_model.set_color(color); - m_model.render(); + for (CutIsland& isl : m_result->cut_islands) { + isl.model.set_color(isl.disabled ? ColorRGBA(1.f, 0.f, 0.f, 1.f) : color); + isl.model.render(); + } shader->stop_using(); } @@ -105,19 +112,80 @@ void MeshClipper::render_cut() } +#if ENABLE_LEGACY_OPENGL_REMOVAL +void MeshClipper::render_contour(const ColorRGBA& color) +#else +void MeshClipper::render_contour() +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ + if (! m_result) + recalculate_triangles(); +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* curr_shader = wxGetApp().get_current_shader(); + if (curr_shader != nullptr) + curr_shader->stop_using(); + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader != nullptr) { + shader->start_using(); + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix()); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + for (CutIsland& isl : m_result->cut_islands) { + isl.model_expanded.set_color(color); + isl.model_expanded.render(); + } + shader->stop_using(); + } + + if (curr_shader != nullptr) + curr_shader->start_using(); +#else + if (m_vertex_array_expanded.has_VBOs()) + m_vertex_array_expanded.render(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +bool MeshClipper::is_projection_inside_cut(const Vec3d& point_in) const +{ + if (!m_result || m_result->cut_islands.empty()) + return false; + Vec3d point = m_result->trafo.inverse() * point_in; + Point pt_2d = Point::new_scale(Vec2d(point.x(), point.y())); + + for (const CutIsland& isl : m_result->cut_islands) { + if (isl.expoly_bb.contains(pt_2d) && isl.expoly.contains(pt_2d)) + return true; + } + return false; +} + +bool MeshClipper::has_valid_contour() const +{ + return m_result && std::any_of(m_result->cut_islands.begin(), m_result->cut_islands.end(), [](const CutIsland& isl) { return !isl.expoly.empty(); }); +} + + +void MeshClipper::pass_mouse_click(const Vec3d& point_in) +{ + if (! m_result || m_result->cut_islands.empty()) + return; + Vec3d point = m_result->trafo.inverse() * point_in; + Point pt_2d = Point::new_scale(Vec2d(point.x(), point.y())); + + for (CutIsland& isl : m_result->cut_islands) { + if (isl.expoly_bb.contains(pt_2d) && isl.expoly.contains(pt_2d)) + isl.disabled = ! isl.disabled; + } +} void MeshClipper::recalculate_triangles() { -#if ENABLE_WORLD_COORDINATE - const Transform3f instance_matrix_no_translation_no_scaling = m_trafo.get_rotation_matrix().cast(); -#else - const Transform3f& instance_matrix_no_translation_no_scaling = m_trafo.get_matrix(true,false,true).cast(); -#endif // ENABLE_WORLD_COORDINATE - // Calculate clipping plane normal in mesh coordinates. - const Vec3f up_noscale = instance_matrix_no_translation_no_scaling.inverse() * m_plane.get_normal().cast(); - const Vec3d up = up_noscale.cast().cwiseProduct(m_trafo.get_scaling_factor()); - // Calculate distance from mesh origin to the clipping plane (in mesh coordinates). - const float height_mesh = m_plane.distance(m_trafo.get_offset()) * (up_noscale.norm()/up.norm()); + m_result = ClipResult(); + + auto plane_mesh = Eigen::Hyperplane(m_plane.get_normal(), -m_plane.distance(Vec3d::Zero())).transform(m_trafo.get_matrix().inverse()); + const Vec3d up = plane_mesh.normal(); + const float height_mesh = -plane_mesh.offset(); // Now do the cutting MeshSlicingParams slicing_params; @@ -137,6 +205,8 @@ void MeshClipper::recalculate_triangles() tr.rotate(q); tr = m_trafo.get_matrix() * tr; + m_result->trafo = tr; + if (m_limiting_plane != ClippingPlane::ClipsNothing()) { // Now remove whatever ended up below the limiting plane (e.g. sinking objects). @@ -190,42 +260,108 @@ void MeshClipper::recalculate_triangles() } } - m_triangles2d = triangulate_expolygons_2f(expolys, m_trafo.get_matrix().matrix().determinant() < 0.); - tr.pretranslate(0.001 * m_plane.get_normal().normalized()); // to avoid z-fighting + Transform3d tr2 = tr; + tr2.pretranslate(0.002 * m_plane.get_normal().normalized()); + #if ENABLE_LEGACY_OPENGL_REMOVAL - m_model.reset(); + std::vector triangles2d; - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; - init_data.reserve_vertices(m_triangles2d.size()); - init_data.reserve_indices(m_triangles2d.size()); + for (const ExPolygon& exp : expolys) { + triangles2d.clear(); - // vertices + indices - for (auto it = m_triangles2d.cbegin(); it != m_triangles2d.cend(); it = it + 3) { - init_data.add_vertex((Vec3f)(tr * Vec3d((*(it + 0)).x(), (*(it + 0)).y(), height_mesh)).cast(), (Vec3f)up.cast()); - init_data.add_vertex((Vec3f)(tr * Vec3d((*(it + 1)).x(), (*(it + 1)).y(), height_mesh)).cast(), (Vec3f)up.cast()); - init_data.add_vertex((Vec3f)(tr * Vec3d((*(it + 2)).x(), (*(it + 2)).y(), height_mesh)).cast(), (Vec3f)up.cast()); - const size_t idx = it - m_triangles2d.cbegin(); - init_data.add_triangle((unsigned int)idx, (unsigned int)idx + 1, (unsigned int)idx + 2); + m_result->cut_islands.push_back(CutIsland()); + CutIsland& isl = m_result->cut_islands.back(); + + if (m_fill_cut) { + triangles2d = triangulate_expolygon_2f(exp, m_trafo.get_matrix().matrix().determinant() < 0.); + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + init_data.reserve_vertices(triangles2d.size()); + init_data.reserve_indices(triangles2d.size()); + + // vertices + indices + for (auto it = triangles2d.cbegin(); it != triangles2d.cend(); it = it + 3) { + init_data.add_vertex((Vec3f)(tr * Vec3d((*(it + 0)).x(), (*(it + 0)).y(), height_mesh)).cast(), (Vec3f)up.cast()); + init_data.add_vertex((Vec3f)(tr * Vec3d((*(it + 1)).x(), (*(it + 1)).y(), height_mesh)).cast(), (Vec3f)up.cast()); + init_data.add_vertex((Vec3f)(tr * Vec3d((*(it + 2)).x(), (*(it + 2)).y(), height_mesh)).cast(), (Vec3f)up.cast()); + const size_t idx = it - triangles2d.cbegin(); + init_data.add_triangle((unsigned int)idx, (unsigned int)idx + 1, (unsigned int)idx + 2); + } + + if (!init_data.is_empty()) + isl.model.init_from(std::move(init_data)); + } + + if (m_contour_width != 0. && ! exp.contour.empty()) { + triangles2d.clear(); + + // The contours must not scale with the object. Check the scale factor + // in the respective directions, create a scaled copy of the ExPolygon + // offset it and then unscale the result again. + + Transform3d t = tr; + t.translation() = Vec3d::Zero(); + double scale_x = (t * Vec3d::UnitX()).norm(); + double scale_y = (t * Vec3d::UnitY()).norm(); + + // To prevent overflow after scaling, downscale the input if needed: + double extra_scale = 1.; + int32_t limit = int32_t(std::min(std::numeric_limits::max() / (2. * scale_x), std::numeric_limits::max() / (2. * scale_y))); + int32_t max_coord = 0; + for (const Point& pt : exp.contour) + max_coord = std::max(max_coord, std::max(std::abs(pt.x()), std::abs(pt.y()))); + if (max_coord + m_contour_width >= limit) + extra_scale = 0.9 * double(limit) / max_coord; + + ExPolygon exp_copy = exp; + if (extra_scale != 1.) + exp_copy.scale(extra_scale); + exp_copy.scale(scale_x, scale_y); + + ExPolygons expolys_exp = offset_ex(exp_copy, scale_(m_contour_width)); + expolys_exp = diff_ex(expolys_exp, ExPolygons({exp_copy})); + + for (ExPolygon& e : expolys_exp) { + e.scale(1./scale_x, 1./scale_y); + if (extra_scale != 1.) + e.scale(1./extra_scale); + } + + + triangles2d = triangulate_expolygons_2f(expolys_exp, m_trafo.get_matrix().matrix().determinant() < 0.); + GLModel::Geometry init_data = GLModel::Geometry(); + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + init_data.reserve_vertices(triangles2d.size()); + init_data.reserve_indices(triangles2d.size()); + + // vertices + indices + for (auto it = triangles2d.cbegin(); it != triangles2d.cend(); it = it + 3) { + init_data.add_vertex((Vec3f)(tr2 * Vec3d((*(it + 0)).x(), (*(it + 0)).y(), height_mesh)).cast(), (Vec3f)up.cast()); + init_data.add_vertex((Vec3f)(tr2 * Vec3d((*(it + 1)).x(), (*(it + 1)).y(), height_mesh)).cast(), (Vec3f)up.cast()); + init_data.add_vertex((Vec3f)(tr2 * Vec3d((*(it + 2)).x(), (*(it + 2)).y(), height_mesh)).cast(), (Vec3f)up.cast()); + const size_t idx = it - triangles2d.cbegin(); + init_data.add_triangle((unsigned short)idx, (unsigned short)idx + 1, (unsigned short)idx + 2); + } + + if (!init_data.is_empty()) + isl.model_expanded.init_from(std::move(init_data)); + } + + isl.expoly = std::move(exp); + isl.expoly_bb = get_extents(exp); } - - if (!init_data.is_empty()) - m_model.init_from(std::move(init_data)); #else - m_vertex_array.release_geometry(); - for (auto it=m_triangles2d.cbegin(); it != m_triangles2d.cend(); it=it+3) { - m_vertex_array.push_geometry(tr * Vec3d((*(it+0))(0), (*(it+0))(1), height_mesh), up); - m_vertex_array.push_geometry(tr * Vec3d((*(it+1))(0), (*(it+1))(1), height_mesh), up); - m_vertex_array.push_geometry(tr * Vec3d((*(it+2))(0), (*(it+2))(1), height_mesh), up); - const size_t idx = it - m_triangles2d.cbegin(); - m_vertex_array.push_triangle(idx, idx+1, idx+2); - } - m_vertex_array.finalize_geometry(true); + #error NOT IMPLEMENTED #endif // ENABLE_LEGACY_OPENGL_REMOVAL - m_triangles_valid = true; + + +#if ENABLE_LEGACY_OPENGL_REMOVAL +#else + #error NOT IMPLEMENTED +#endif // ENABLE_LEGACY_OPENGL_REMOVAL } @@ -239,7 +375,7 @@ void MeshRaycaster::line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3 Vec3d& point, Vec3d& direction) #else void MeshRaycaster::line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, - Vec3d& point, Vec3d& direction) const + Vec3d& point, Vec3d& direction) #endif // ENABLE_RAYCAST_PICKING { Matrix4d modelview = camera.get_view_matrix().matrix(); @@ -264,8 +400,11 @@ void MeshRaycaster::line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3 bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, Vec3f& position, Vec3f& normal, const ClippingPlane* clipping_plane, - size_t* facet_idx) const + size_t* facet_idx, bool* was_clipping_plane_hit) const { + if (was_clipping_plane_hit) + *was_clipping_plane_hit = false; + Vec3d point; Vec3d direction; line_from_mouse_pos(mouse_pos, trafo, camera, point, direction); @@ -286,9 +425,26 @@ bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& break; } - if (i==hits.size() || (hits.size()-i) % 2 != 0) { - // All hits are either clipped, or there is an odd number of unclipped - // hits - meaning the nearest must be from inside the mesh. + if (i==hits.size()) { + // All hits are clipped. + return false; + } + if ((hits.size()-i) % 2 != 0) { + // There is an odd number of unclipped hits - meaning the nearest must be from inside the mesh. + // In that case, calculate intersection with the clipping place. + if (clipping_plane && was_clipping_plane_hit) { + direction = direction + point; + point = trafo * point; // transform to world coords + direction = trafo * direction - point; + + Vec3d normal = -clipping_plane->get_normal().cast(); + double den = normal.dot(direction); + if (den != 0.) { + double t = (-clipping_plane->get_offset() - normal.dot(point))/den; + position = (point + t * direction).cast(); + *was_clipping_plane_hit = true; + } + } return false; } @@ -302,6 +458,35 @@ bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& return true; } +bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, + Vec3d& position, Vec3d& normal) const +{ + Vec3d point; + Vec3d direction; + line_from_mouse_pos(mouse_pos, trafo, camera, point, direction); + + std::vector hits = m_emesh.query_ray_hits(point, direction); + + if (hits.empty()) + return false; // no intersection found + + // Now stuff the points in the provided vector and calculate normals if asked about them: + position = hits[0].position(); + normal = hits[0].normal(); + + return true; +} + +bool MeshRaycaster::is_valid_intersection(Vec3d point, Vec3d direction, const Transform3d& trafo) const +{ + point = trafo.inverse() * point; + + std::vector hits = m_emesh.query_ray_hits(point, direction); + std::vector neg_hits = m_emesh.query_ray_hits(point, -direction); + + return !hits.empty() && !neg_hits.empty(); +} + std::vector MeshRaycaster::get_unobscured_idxs(const Geometry::Transformation& trafo, const Camera& camera, const std::vector& points, const ClippingPlane* clipping_plane) const diff --git a/src/slic3r/GUI/MeshUtils.hpp b/src/slic3r/GUI/MeshUtils.hpp index 9f7d394a9..9db2ed1b1 100644 --- a/src/slic3r/GUI/MeshUtils.hpp +++ b/src/slic3r/GUI/MeshUtils.hpp @@ -14,6 +14,7 @@ #endif // ENABLE_LEGACY_OPENGL_REMOVAL #include +#include #if ENABLE_RAYCAST_PICKING #include #endif // ENABLE_RAYCAST_PICKING @@ -80,6 +81,10 @@ public: class MeshClipper { public: + // Set whether the cut should be triangulated and whether a cut + // contour should be calculated and shown. + void set_behaviour(bool fill_cut, double contour_width); + // Inform MeshClipper about which plane we want to use to cut the mesh // This is supposed to be in world coordinates. void set_plane(const ClippingPlane& plane); @@ -103,10 +108,16 @@ public: // be set in world coords. #if ENABLE_LEGACY_OPENGL_REMOVAL void render_cut(const ColorRGBA& color); + void render_contour(const ColorRGBA& color); #else void render_cut(); #endif // ENABLE_LEGACY_OPENGL_REMOVAL + void pass_mouse_click(const Vec3d& pt); + + bool is_projection_inside_cut(const Vec3d& point) const; + bool has_valid_contour() const; + private: void recalculate_triangles(); @@ -115,13 +126,27 @@ private: const TriangleMesh* m_negative_mesh = nullptr; ClippingPlane m_plane; ClippingPlane m_limiting_plane = ClippingPlane::ClipsNothing(); - std::vector m_triangles2d; #if ENABLE_LEGACY_OPENGL_REMOVAL - GLModel m_model; + + struct CutIsland { + GLModel model; + GLModel model_expanded; + ExPolygon expoly; + BoundingBox expoly_bb; + bool disabled = false; + }; + struct ClipResult { + std::vector cut_islands; + Transform3d trafo; // this rotates the cut into world coords + }; + std::optional m_result; + #else + #error NOT IMLEMENTED GLIndexedVertexArray m_vertex_array; #endif // ENABLE_LEGACY_OPENGL_REMOVAL - bool m_triangles_valid = false; + bool m_fill_cut = true; + double m_contour_width = 0.; }; @@ -150,8 +175,10 @@ public: { } - void line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, - Vec3d& point, Vec3d& direction) const; + static void line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, + Vec3d& point, Vec3d& direction); +// void line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, +// Vec3d& point, Vec3d& direction) const; #endif // ENABLE_RAYCAST_PICKING // Given a mouse position, this returns true in case it is on the mesh. @@ -159,12 +186,18 @@ public: const Vec2d& mouse_pos, const Transform3d& trafo, // how to get the mesh into world coords const Camera& camera, // current camera position - Vec3f& position, // where to save the positibon of the hit (mesh coords) + Vec3f& position, // where to save the positibon of the hit (mesh coords if mesh, world coords if clipping plane) Vec3f& normal, // normal of the triangle that was hit const ClippingPlane* clipping_plane = nullptr, // clipping plane (if active) - size_t* facet_idx = nullptr // index of the facet hit + size_t* facet_idx = nullptr, // index of the facet hit + bool* was_clipping_plane_hit = nullptr // is the hit on the clipping place cross section? ) const; + // Given a mouse position, this returns true in case it is on the mesh. + bool unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, Vec3d& position, Vec3d& normal) const; + + bool is_valid_intersection(Vec3d point, Vec3d direction, const Transform3d& trafo) const; + // Given a vector of points in woorld coordinates, this returns vector // of indices of points that are visible (i.e. not cut by clipping plane // or obscured by part of the mesh. diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 2e74270da..594e0716c 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1228,6 +1228,7 @@ void NotificationManager::UpdatedItemsInfoNotification::add_type(InfoItemType ty case InfoItemType::MmuSegmentation: text += format(_L_PLURAL("%1$d object was loaded with multimaterial painting.", "%1$d objects were loaded with multimaterial painting.",(*it).second), (*it).second) + "\n"; break; case InfoItemType::VariableLayerHeight: text += format(_L_PLURAL("%1$d object was loaded with variable layer height.", "%1$d objects were loaded with variable layer height.", (*it).second), (*it).second) + "\n"; break; case InfoItemType::Sinking: text += format(_L_PLURAL("%1$d object was loaded with partial sinking.", "%1$d objects were loaded with partial sinking.", (*it).second), (*it).second) + "\n"; break; + case InfoItemType::CutConnectors: text += format(_L_PLURAL("%1$d object was loaded as a part of cut object.", "%1$d objects were loaded as parts of cut object", (*it).second), (*it).second) + "\n"; break; default: BOOST_LOG_TRIVIAL(error) << "Unknown InfoItemType: " << (*it).second; break; } } diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index ba70f0ebd..50577771b 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -34,10 +34,19 @@ void ObjectDataViewModelNode::init_container() #endif //__WXGTK__ } +void ObjectDataViewModelNode::invalidate_container() +{ +#ifndef __WXGTK__ + if (this->GetChildCount() == 0) + this->m_container = false; +#endif //__WXGTK__ +} + static constexpr char LayerRootIcon[] = "edit_layers_all"; static constexpr char LayerIcon[] = "edit_layers_some"; static constexpr char WarningIcon[] = "exclamation"; static constexpr char WarningManifoldIcon[] = "exclamation_manifold"; +static constexpr char LockIcon[] = "cut_"; struct InfoItemAtributes { std::string name; @@ -48,6 +57,7 @@ const std::map INFO_ITEMS{ // info_item Type info_item Name info_item BitmapName { InfoItemType::CustomSupports, {L("Paint-on supports"), "fdm_supports_" }, }, { InfoItemType::CustomSeam, {L("Paint-on seam"), "seam_" }, }, + { InfoItemType::CutConnectors, {L("Cut connectors"), "cut_connectors" }, }, { InfoItemType::MmuSegmentation, {L("Multimaterial painting"), "mmu_segmentation_"}, }, { InfoItemType::Sinking, {L("Sinking"), "sinking"}, }, { InfoItemType::VariableLayerHeight, {L("Variable layer height"), "layers"}, }, @@ -56,19 +66,15 @@ const std::map INFO_ITEMS{ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const wxString& sub_obj_name, Slic3r::ModelVolumeType type, - const wxBitmapBundle& bmp, const wxString& extruder, - const int idx/* = -1*/, - const std::string& warning_icon_name /*= std::string*/) : + const int idx/* = -1*/) : m_parent(parent), m_name(sub_obj_name), m_type(itVolume), m_volume_type(type), m_idx(idx), - m_extruder(type == Slic3r::ModelVolumeType::MODEL_PART || type == Slic3r::ModelVolumeType::PARAMETER_MODIFIER ? extruder : ""), - m_warning_icon_name(warning_icon_name) + m_extruder(type == Slic3r::ModelVolumeType::MODEL_PART || type == Slic3r::ModelVolumeType::PARAMETER_MODIFIER ? extruder : "") { - m_bmp = bmp; set_action_and_extruder_icons(); init_container(); } @@ -173,13 +179,6 @@ void ObjectDataViewModelNode::set_printable_icon(PrintIndicator printable) *get_bmp_bundle(m_printable == piPrintable ? "eye_open" : "eye_closed"); } -void ObjectDataViewModelNode::set_warning_icon(const std::string& warning_icon_name) -{ - m_warning_icon_name = warning_icon_name; - if (warning_icon_name.empty()) - m_bmp = m_empty_bmp; -} - void ObjectDataViewModelNode::update_settings_digest_bitmaps() { m_bmp = m_empty_bmp; @@ -327,6 +326,7 @@ ObjectDataViewModel::ObjectDataViewModel() m_volume_bmps = MenuFactory::get_volume_bitmaps(); m_warning_bmp = *get_bmp_bundle(WarningIcon); m_warning_manifold_bmp = *get_bmp_bundle(WarningManifoldIcon); + m_lock_bmp = *get_bmp_bundle(LockIcon); for (auto item : INFO_ITEMS) m_info_bmps[item.first] = get_bmp_bundle(item.second.bmp_name); @@ -340,19 +340,55 @@ ObjectDataViewModel::~ObjectDataViewModel() m_bitmap_cache = nullptr; } -wxBitmapBundle& ObjectDataViewModel::GetWarningBitmap(const std::string& warning_icon_name) +void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode* node) { - return warning_icon_name.empty() ? m_empty_bmp : warning_icon_name == WarningIcon ? m_warning_bmp : m_warning_manifold_bmp; + int vol_type = static_cast(node->GetVolumeType()); + bool is_volume_node = vol_type >= 0; + + if (!node->has_warning_icon() && !node->has_lock()) { + node->SetBitmap(is_volume_node ? *m_volume_bmps.at(vol_type) : m_empty_bmp); + return; + } + + std::string scaled_bitmap_name = std::string(); + if (node->has_warning_icon()) + scaled_bitmap_name += node->warning_icon_name(); + if (node->has_lock()) + scaled_bitmap_name += LockIcon; + if (is_volume_node) + scaled_bitmap_name += std::to_string(vol_type); + scaled_bitmap_name += (wxGetApp().dark_mode() ? "-dm" : "-lm"); + + wxBitmapBundle* bmp = m_bitmap_cache->find_bndl(scaled_bitmap_name); + if (!bmp) { + std::vector bmps; + if (node->has_warning_icon()) + bmps.emplace_back(node->warning_icon_name() == WarningIcon ? &m_warning_bmp : &m_warning_manifold_bmp); + if (node->has_lock()) + bmps.emplace_back(&m_lock_bmp); + if (is_volume_node) + bmps.emplace_back(m_volume_bmps[vol_type]); + bmp = m_bitmap_cache->insert_bndl(scaled_bitmap_name, bmps); + } + + node->SetBitmap(*bmp); } -wxDataViewItem ObjectDataViewModel::Add(const wxString &name, - const int extruder, - const std::string& warning_icon_name/* = std::string()*/ ) +void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode* node, const std::string& warning_icon_name, bool has_lock) { - const wxString extruder_str = extruder == 0 ? _L("default") : wxString::Format("%d", extruder); - auto root = new ObjectDataViewModelNode(name, extruder_str); + node->SetWarningIconName(warning_icon_name); + node->SetLock(has_lock); + UpdateBitmapForNode(node); +} + +wxDataViewItem ObjectDataViewModel::AddObject(const wxString &name, + const wxString& extruder, + const std::string& warning_icon_name, + const bool has_lock) +{ + auto root = new ObjectDataViewModelNode(name, extruder); // Add warning icon if detected auto-repaire - root->SetWarningBitmap(GetWarningBitmap(warning_icon_name), warning_icon_name); + UpdateBitmapForNode(root, warning_icon_name, has_lock); m_objects.push_back(root); // notify control @@ -365,42 +401,29 @@ wxDataViewItem ObjectDataViewModel::Add(const wxString &name, wxDataViewItem ObjectDataViewModel::AddVolumeChild( const wxDataViewItem &parent_item, const wxString &name, + const int volume_idx, const Slic3r::ModelVolumeType volume_type, - const std::string& warning_icon_name/* = std::string()*/, - const int extruder/* = 0*/, - const bool create_frst_child/* = true*/) + const std::string& warning_icon_name, + const wxString& extruder) { ObjectDataViewModelNode *root = static_cast(parent_item.GetID()); if (!root) return wxDataViewItem(0); - wxString extruder_str = extruder == 0 ? _(L("default")) : wxString::Format("%d", extruder); - // get insertion position according to the existed Layers and/or Instances Items int insert_position = get_root_idx(root, itLayerRoot); if (insert_position < 0) insert_position = get_root_idx(root, itInstanceRoot); - if (create_frst_child && root->m_volumes_cnt == 0) - { - const Slic3r::ModelVolumeType type = Slic3r::ModelVolumeType::MODEL_PART; - const auto node = new ObjectDataViewModelNode(root, root->m_name, type, GetVolumeIcon(type, root->m_warning_icon_name), extruder_str, 0, root->m_warning_icon_name); - - insert_position < 0 ? root->Append(node) : root->Insert(node, insert_position); - // notify control - const wxDataViewItem child((void*)node); - ItemAdded(parent_item, child); - - root->m_volumes_cnt++; - if (insert_position >= 0) insert_position++; - } - - const auto node = new ObjectDataViewModelNode(root, name, volume_type, GetVolumeIcon(volume_type, warning_icon_name), extruder_str, root->m_volumes_cnt, warning_icon_name); + const auto node = new ObjectDataViewModelNode(root, name, volume_type, extruder, volume_idx); + UpdateBitmapForNode(node, warning_icon_name, root->has_lock() && volume_type < ModelVolumeType::PARAMETER_MODIFIER); insert_position < 0 ? root->Append(node) : root->Insert(node, insert_position); // if part with errors is added, but object wasn't marked, then mark it - if (!warning_icon_name.empty() && warning_icon_name != root->m_warning_icon_name && - (root->m_warning_icon_name.empty() || root->m_warning_icon_name == WarningManifoldIcon) ) - root->SetWarningBitmap(GetWarningBitmap(warning_icon_name), warning_icon_name); + if (!warning_icon_name.empty() && warning_icon_name != root->warning_icon_name() && + (!root->has_warning_icon() || root->warning_icon_name() == WarningManifoldIcon)) { + root->SetWarningIconName(warning_icon_name); + UpdateBitmapForNode(root); + } // notify control const wxDataViewItem child((void*)node); @@ -598,14 +621,12 @@ wxDataViewItem ObjectDataViewModel::AddLayersRoot(const wxDataViewItem &parent_i wxDataViewItem ObjectDataViewModel::AddLayersChild(const wxDataViewItem &parent_item, const t_layer_height_range& layer_range, - const int extruder/* = 0*/, + const wxString& extruder, const int index /* = -1*/) { ObjectDataViewModelNode *parent_node = static_cast(parent_item.GetID()); if (!parent_node) return wxDataViewItem(0); - wxString extruder_str = extruder == 0 ? _(L("default")) : wxString::Format("%d", extruder); - // get LayerRoot node ObjectDataViewModelNode *layer_root_node; wxDataViewItem layer_root_item; @@ -622,7 +643,7 @@ wxDataViewItem ObjectDataViewModel::AddLayersChild(const wxDataViewItem &parent_ } // Add layer node - ObjectDataViewModelNode *layer_node = new ObjectDataViewModelNode(layer_root_node, layer_range, index, extruder_str); + ObjectDataViewModelNode *layer_node = new ObjectDataViewModelNode(layer_root_node, layer_range, index, extruder); if (index < 0) layer_root_node->Append(layer_node); else @@ -711,10 +732,7 @@ wxDataViewItem ObjectDataViewModel::Delete(const wxDataViewItem &item) delete node_parent; ret_item = wxDataViewItem(obj_node); -#ifndef __WXGTK__ - if (obj_node->GetChildCount() == 0) - obj_node->m_container = false; -#endif //__WXGTK__ + obj_node->invalidate_container(); ItemDeleted(ret_item, wxDataViewItem(node_parent)); return ret_item; } @@ -730,10 +748,7 @@ wxDataViewItem ObjectDataViewModel::Delete(const wxDataViewItem &item) delete node_parent; ret_item = wxDataViewItem(obj_node); -#ifndef __WXGTK__ - if (obj_node->GetChildCount() == 0) - obj_node->m_container = false; -#endif //__WXGTK__ + obj_node->invalidate_container(); ItemDeleted(ret_item, wxDataViewItem(node_parent)); return ret_item; } @@ -755,10 +770,7 @@ wxDataViewItem ObjectDataViewModel::Delete(const wxDataViewItem &item) node_parent->m_volumes_cnt = 0; delete last_child_node; -#ifndef __WXGTK__ - if (node_parent->GetChildCount() == 0) - node_parent->m_container = false; -#endif //__WXGTK__ + node_parent->invalidate_container(); ItemDeleted(parent, wxDataViewItem(last_child_node)); wxCommandEvent event(wxCUSTOMEVT_LAST_VOLUME_IS_DELETED); @@ -793,10 +805,7 @@ wxDataViewItem ObjectDataViewModel::Delete(const wxDataViewItem &item) // set m_containet to FALSE if parent has no child if (node_parent) { -#ifndef __WXGTK__ - if (node_parent->GetChildCount() == 0) - node_parent->m_container = false; -#endif //__WXGTK__ + node_parent->invalidate_container(); ret_item = parent; } @@ -838,10 +847,7 @@ wxDataViewItem ObjectDataViewModel::DeleteLastInstance(const wxDataViewItem &par parent_node->set_printable_icon(last_inst_printable); ItemDeleted(parent_item, inst_root_item); ItemChanged(parent_item); -#ifndef __WXGTK__ - if (parent_node->GetChildCount() == 0) - parent_node->m_container = false; -#endif //__WXGTK__ + parent_node->invalidate_container(); } // update object_node printable property @@ -886,10 +892,7 @@ void ObjectDataViewModel::DeleteChildren(wxDataViewItem& parent) ItemDeleted(parent, item); } - // set m_containet to FALSE if parent has no child -#ifndef __WXGTK__ - root->m_container = false; -#endif //__WXGTK__ + root->invalidate_container(); } void ObjectDataViewModel::DeleteVolumeChildren(wxDataViewItem& parent) @@ -919,11 +922,7 @@ void ObjectDataViewModel::DeleteVolumeChildren(wxDataViewItem& parent) ItemDeleted(parent, item); } root->m_volumes_cnt = 0; - - // set m_containet to FALSE if parent has no child -#ifndef __WXGTK__ - root->m_container = false; -#endif //__WXGTK__ + root->invalidate_container(); } void ObjectDataViewModel::DeleteSettings(const wxDataViewItem& parent) @@ -1681,6 +1680,7 @@ void ObjectDataViewModel::UpdateBitmaps() m_volume_bmps = MenuFactory::get_volume_bitmaps(); m_warning_bmp = *get_bmp_bundle(WarningIcon); m_warning_manifold_bmp = *get_bmp_bundle(WarningManifoldIcon); + m_lock_bmp = *get_bmp_bundle(LockIcon); for (auto item : INFO_ITEMS) m_info_bmps[item.first] = get_bmp_bundle(item.second.bmp_name); @@ -1699,10 +1699,8 @@ void ObjectDataViewModel::UpdateBitmaps() switch (node->m_type) { case itObject: - if (node->m_bmp.IsOk()) node->m_bmp = GetWarningBitmap(node->m_warning_icon_name); - break; case itVolume: - node->m_bmp = GetVolumeIcon(node->m_volume_type, node->m_warning_icon_name); + UpdateBitmapForNode(node); break; case itLayerRoot: node->m_bmp = *get_bmp_bundle(LayerRootIcon); @@ -1720,27 +1718,6 @@ void ObjectDataViewModel::UpdateBitmaps() } } -wxBitmapBundle ObjectDataViewModel::GetVolumeIcon(const Slic3r::ModelVolumeType vol_type, const std::string& warning_icon_name/* = std::string()*/) -{ - if (warning_icon_name.empty()) - return *m_volume_bmps[static_cast(vol_type)]; - - std::string scaled_bitmap_name = warning_icon_name + std::to_string(static_cast(vol_type)); - scaled_bitmap_name += "-em" + std::to_string(wxGetApp().em_unit()) + (wxGetApp().dark_mode() ? "-dm" : "-lm"); - - wxBitmapBundle *bmp = m_bitmap_cache->find_bndl(scaled_bitmap_name); - if (bmp == nullptr) { - std::vector bmps; - - bmps.emplace_back(&GetWarningBitmap(warning_icon_name)); - bmps.emplace_back(m_volume_bmps[static_cast(vol_type)]); - - bmp = m_bitmap_cache->insert_bndl(scaled_bitmap_name, bmps); - } - - return *bmp; -} - void ObjectDataViewModel::AddWarningIcon(const wxDataViewItem& item, const std::string& warning_icon_name) { if (!item.IsOk()) @@ -1748,13 +1725,14 @@ void ObjectDataViewModel::AddWarningIcon(const wxDataViewItem& item, const std:: ObjectDataViewModelNode *node = static_cast(item.GetID()); if (node->GetType() & itObject) { - node->SetWarningBitmap(GetWarningBitmap(warning_icon_name), warning_icon_name); + UpdateBitmapForNode(node, warning_icon_name, node->has_lock()); return; } if (node->GetType() & itVolume) { - node->SetWarningBitmap(GetVolumeIcon(node->GetVolumeType(), warning_icon_name), warning_icon_name); - node->GetParent()->SetWarningBitmap(GetWarningBitmap(warning_icon_name), warning_icon_name); + UpdateBitmapForNode(node, warning_icon_name, node->has_lock()); + if (ObjectDataViewModelNode* parent = node->GetParent()) + UpdateBitmapForNode(parent, warning_icon_name, parent->has_lock()); return; } } @@ -1769,12 +1747,9 @@ void ObjectDataViewModel::DeleteWarningIcon(const wxDataViewItem& item, const bo if (!node->GetBitmap().IsOk() || !(node->GetType() & (itVolume | itObject))) return; - if (node->GetType() & itVolume) { - node->SetWarningBitmap(*m_volume_bmps[static_cast(node->volume_type())], ""); - return; - } + node->SetWarningIconName(std::string()); + UpdateBitmapForNode(node); - node->SetWarningBitmap(wxNullBitmap, ""); if (unmark_object) { wxDataViewItemArray children; @@ -1801,6 +1776,26 @@ void ObjectDataViewModel::UpdateWarningIcon(const wxDataViewItem& item, const st AddWarningIcon(item, warning_icon_name); } +void ObjectDataViewModel::UpdateLockIcon(const wxDataViewItem& item, bool has_lock) +{ + if (!item.IsOk()) + return; + ObjectDataViewModelNode* node = static_cast(item.GetID()); + if (node->has_lock() == has_lock) + return; + + node->SetLock(has_lock); + UpdateBitmapForNode(node); + + if (node->GetType() & itObject) { + wxDataViewItemArray children; + GetChildren(item, children); + for (const wxDataViewItem& child : children) + UpdateLockIcon(child, has_lock); + } + ItemChanged(item); +} + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/ObjectDataViewModel.hpp b/src/slic3r/GUI/ObjectDataViewModel.hpp index 7014acccb..6f2d1519c 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.hpp +++ b/src/slic3r/GUI/ObjectDataViewModel.hpp @@ -51,6 +51,7 @@ enum class InfoItemType Undef, CustomSupports, CustomSeam, + CutConnectors, MmuSegmentation, Sinking, VariableLayerHeight @@ -79,9 +80,10 @@ class ObjectDataViewModelNode PrintIndicator m_printable {piUndef}; wxBitmapBundle m_printable_icon; std::string m_warning_icon_name{ "" }; + bool m_has_lock{false}; std::string m_action_icon_name = ""; - ModelVolumeType m_volume_type; + ModelVolumeType m_volume_type{ -1 }; InfoItemType m_info_item_type {InfoItemType::Undef}; public: @@ -99,10 +101,8 @@ public: ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const wxString& sub_obj_name, Slic3r::ModelVolumeType type, - const wxBitmapBundle& bmp, const wxString& extruder, - const int idx = -1, - const std::string& warning_icon_name = std::string()); + const int idx = -1 ); ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const t_layer_height_range& layer_range, @@ -128,7 +128,9 @@ public: } void init_container(); - bool IsContainer() const + void invalidate_container(); + + bool IsContainer() const { return m_container; } @@ -181,7 +183,8 @@ public: void SetVolumeType(ModelVolumeType type) { m_volume_type = type; } void SetBitmap(const wxBitmapBundle &icon) { m_bmp = icon; } void SetExtruder(const wxString &extruder) { m_extruder = extruder; } - void SetWarningBitmap(const wxBitmapBundle& icon, const std::string& warning_icon_name) { m_bmp = icon; m_warning_icon_name = warning_icon_name; } + void SetWarningIconName(const std::string& warning_icon_name) { m_warning_icon_name = warning_icon_name; } + void SetLock(bool has_lock) { m_has_lock = has_lock; } const wxBitmapBundle& GetBitmap() const { return m_bmp; } const wxString& GetName() const { return m_name; } ItemType GetType() const { return m_type; } @@ -228,8 +231,6 @@ public: void set_extruder_icon(); // Set printable icon for node void set_printable_icon(PrintIndicator printable); - // Set warning icon for node - void set_warning_icon(const std::string& warning_icon); void update_settings_digest_bitmaps(); bool update_settings_digest(const std::vector& categories); @@ -240,7 +241,9 @@ public: bool valid(); #endif /* NDEBUG */ bool invalid() const { return m_idx < -1; } - bool has_warning_icon() const { return !m_warning_icon_name.empty(); } + bool has_warning_icon() const { return !m_warning_icon_name.empty(); } + bool has_lock() const { return m_has_lock; } + const std::string& warning_icon_name() const { return m_warning_icon_name; } private: friend class ObjectDataViewModel; @@ -262,6 +265,7 @@ class ObjectDataViewModel :public wxDataViewModel wxBitmapBundle m_empty_bmp; wxBitmapBundle m_warning_bmp; wxBitmapBundle m_warning_manifold_bmp; + wxBitmapBundle m_lock_bmp; wxDataViewCtrl* m_ctrl { nullptr }; @@ -269,15 +273,16 @@ public: ObjectDataViewModel(); ~ObjectDataViewModel(); - wxDataViewItem Add( const wxString &name, - const int extruder, - const std::string& warning_icon_name = std::string()); + wxDataViewItem AddObject( const wxString &name, + const wxString& extruder, + const std::string& warning_icon_name, + const bool has_lock); wxDataViewItem AddVolumeChild( const wxDataViewItem &parent_item, const wxString &name, + const int volume_idx, const Slic3r::ModelVolumeType volume_type, - const std::string& warning_icon_name = std::string(), - const int extruder = 0, - const bool create_frst_child = true); + const std::string& warning_icon_name, + const wxString& extruder); wxDataViewItem AddSettingsChild(const wxDataViewItem &parent_item); wxDataViewItem AddInfoChild(const wxDataViewItem &parent_item, InfoItemType info_type); wxDataViewItem AddInstanceChild(const wxDataViewItem &parent_item, size_t num); @@ -285,7 +290,7 @@ public: wxDataViewItem AddLayersRoot(const wxDataViewItem &parent_item); wxDataViewItem AddLayersChild( const wxDataViewItem &parent_item, const t_layer_height_range& layer_range, - const int extruder = 0, + const wxString& extruder, const int index = -1); size_t GetItemIndexForFirstVolume(ObjectDataViewModelNode* node_parent); wxDataViewItem Delete(const wxDataViewItem &item); @@ -390,6 +395,7 @@ public: void AddWarningIcon(const wxDataViewItem& item, const std::string& warning_name); void DeleteWarningIcon(const wxDataViewItem& item, const bool unmark_object = false); void UpdateWarningIcon(const wxDataViewItem& item, const std::string& warning_name); + void UpdateLockIcon(const wxDataViewItem& item, bool has_lock); bool HasWarningIcon(const wxDataViewItem& item) const; t_layer_height_range GetLayerRangeByItem(const wxDataViewItem& item) const; @@ -403,7 +409,8 @@ private: wxDataViewItem AddInstanceRoot(const wxDataViewItem& parent_item); void AddAllChildren(const wxDataViewItem& parent); - wxBitmapBundle& GetWarningBitmap(const std::string& warning_icon_name); + void UpdateBitmapForNode(ObjectDataViewModelNode* node); + void UpdateBitmapForNode(ObjectDataViewModelNode* node, const std::string& warning_icon_name, bool has_lock); }; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index eb8b5b01a..22c819ab4 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -97,6 +97,7 @@ #include "MsgDialog.hpp" #include "ProjectDirtyStateManager.hpp" #include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification +#include "Gizmos/GLGizmoCut.hpp" #ifdef __APPLE__ #include "Gizmos/GLGizmosManager.hpp" @@ -1792,7 +1793,7 @@ struct Plater::priv std::string get_config(const std::string &key) const; std::vector load_files(const std::vector& input_files, bool load_model, bool load_config, bool used_inches = false); - std::vector load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z = false); + std::vector load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z = false, bool call_selection_changed = true); fs::path get_export_file_path(GUI::FileType file_type); wxString get_export_file(GUI::FileType file_type); @@ -1807,7 +1808,7 @@ struct Plater::priv void select_all(); void deselect_all(); void remove(size_t obj_idx); - void delete_object_from_model(size_t obj_idx); + bool delete_object_from_model(size_t obj_idx); void delete_all_objects_from_model(); void reset(); void mirror(Axis axis); @@ -2718,7 +2719,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ // #define AUTOPLACEMENT_ON_LOAD -std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z) +std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z, bool call_selection_changed /*= true*/) { const Vec3d bed_size = Slic3r::to_3d(this->bed.build_volume().bounding_volume2d().size(), 1.0) - 2.0 * Vec3d::Ones(); @@ -2801,17 +2802,18 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode notification_manager->close_notification_of_type(NotificationType::UpdatedItemsInfo); for (const size_t idx : obj_idxs) { - wxGetApp().obj_list()->add_object_to_list(idx); + wxGetApp().obj_list()->add_object_to_list(idx, call_selection_changed); } - update(); - // Update InfoItems in ObjectList after update() to use of a correct value of the GLCanvas3D::is_sinking(), - // which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call - for (const size_t idx : obj_idxs) - wxGetApp().obj_list()->update_info_items(idx); - - object_list_changed(); + if (call_selection_changed) { + update(); + // Update InfoItems in ObjectList after update() to use of a correct value of the GLCanvas3D::is_sinking(), + // which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call + for (const size_t idx : obj_idxs) + wxGetApp().obj_list()->update_info_items(idx); + object_list_changed(); + } this->schedule_background_process(); return obj_idxs; @@ -2990,16 +2992,35 @@ void Plater::priv::remove(size_t obj_idx) } -void Plater::priv::delete_object_from_model(size_t obj_idx) +bool Plater::priv::delete_object_from_model(size_t obj_idx) { + // check if object isn't cut + // show warning message that "cut consistancy" will not be supported any more + ModelObject* obj = model.objects[obj_idx]; + if (obj->is_cut()) { + InfoDialog dialog(q, _L("Delete object which is a part of cut object"), + _L("You try to delete an object which is a part of a cut object.\n" + "This action will break a cut correspondence.\n" + "After that PrusaSlicer can't garantie model consistency"), + false, wxYES | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING); + dialog.SetButtonLabel(wxID_YES, _L("Delete object")); + if (dialog.ShowModal() == wxID_CANCEL) + return false; + } + wxString snapshot_label = _L("Delete Object"); - if (! model.objects[obj_idx]->name.empty()) - snapshot_label += ": " + wxString::FromUTF8(model.objects[obj_idx]->name.c_str()); + if (!obj->name.empty()) + snapshot_label += ": " + wxString::FromUTF8(obj->name.c_str()); Plater::TakeSnapshot snapshot(q, snapshot_label); m_worker.cancel_all(); + + if (obj->is_cut()) + sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx); + model.delete_object(obj_idx); update(); object_list_changed(); + return true; } void Plater::priv::delete_all_objects_from_model() @@ -4438,7 +4459,10 @@ void Plater::priv::on_action_split_volumes(SimpleEvent&) void Plater::priv::on_action_layersediting(SimpleEvent&) { - view3D->enable_layers_editing(!view3D->is_layers_editing_enabled()); + const bool enable_layersediting = !view3D->is_layers_editing_enabled(); + view3D->enable_layers_editing(enable_layersediting); + if (enable_layersediting) + view3D->get_canvas3d()->reset_all_gizmos(); notification_manager->set_move_from_overlay(view3D->is_layers_editing_enabled()); } @@ -4481,7 +4505,7 @@ void Plater::priv::on_right_click(RBtnEvent& evt) selection.is_single_full_object() || selection.is_multiple_full_instance(); #if ENABLE_WORLD_COORDINATE - const bool is_part = selection.is_single_volume_or_modifier(); + const bool is_part = selection.is_single_volume_or_modifier() && ! selection.is_any_connector(); #else const bool is_part = selection.is_single_volume() || selection.is_single_modifier(); #endif // ENABLE_WORLD_COORDINATE @@ -4753,7 +4777,8 @@ bool Plater::priv::can_split(bool to_objects) const bool Plater::priv::can_scale_to_print_volume() const { const BuildVolume::Type type = this->bed.build_volume().type(); - return !view3D->get_canvas3d()->get_selection().is_empty() && (type == BuildVolume::Type::Rectangle || type == BuildVolume::Type::Circle); + return !sidebar->obj_list()->has_selected_cut_object() && + !view3D->get_canvas3d()->get_selection().is_empty() && (type == BuildVolume::Type::Rectangle || type == BuildVolume::Type::Circle); } bool Plater::priv::layers_height_allowed() const @@ -4768,16 +4793,19 @@ bool Plater::priv::layers_height_allowed() const bool Plater::priv::can_mirror() const { - return get_selection().is_from_single_instance(); + return !sidebar->obj_list()->has_selected_cut_object() && get_selection().is_from_single_instance(); } bool Plater::priv::can_replace_with_stl() const { - return get_selection().get_volume_idxs().size() == 1; + return !sidebar->obj_list()->has_selected_cut_object() && get_selection().get_volume_idxs().size() == 1; } bool Plater::priv::can_reload_from_disk() const { + if (sidebar->obj_list()->has_selected_cut_object()) + return false; + #if ENABLE_RELOAD_FROM_DISK_REWORK // collect selected reloadable ModelVolumes std::vector> selected_volumes = reloadable_volumes(model, get_selection()); @@ -4891,8 +4919,12 @@ bool Plater::priv::can_fix_through_netfabb() const bool Plater::priv::can_simplify() const { + const int obj_idx = get_selected_object_idx(); // is object for simplification selected - if (get_selected_object_idx() < 0) return false; + // cut object can't be simplify + if (obj_idx < 0 || model.objects[obj_idx]->is_cut()) + return false; + // is already opened? if (q->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::EType::Simplify) @@ -4906,8 +4938,7 @@ bool Plater::priv::can_increase_instances() const || q->canvas3D()->get_gizmos_manager().is_in_editing_mode()) return false; - int obj_idx = get_selected_object_idx(); - return (0 <= obj_idx) && (obj_idx < (int)model.objects.size()); + return !sidebar->obj_list()->has_selected_cut_object(); } bool Plater::priv::can_decrease_instances() const @@ -4916,8 +4947,7 @@ bool Plater::priv::can_decrease_instances() const || q->canvas3D()->get_gizmos_manager().is_in_editing_mode()) return false; - int obj_idx = get_selected_object_idx(); - return (0 <= obj_idx) && (obj_idx < (int)model.objects.size()) && (model.objects[obj_idx]->instances.size() > 1); + return !sidebar->obj_list()->has_selected_cut_object(); } bool Plater::priv::can_split_to_objects() const @@ -5721,7 +5751,7 @@ void Plater::reset_with_confirm() reset(); } -void Plater::delete_object_from_model(size_t obj_idx) { p->delete_object_from_model(obj_idx); } +bool Plater::delete_object_from_model(size_t obj_idx) { return p->delete_object_from_model(obj_idx); } void Plater::remove_selected() { @@ -5898,23 +5928,29 @@ void Plater::toggle_layers_editing(bool enable) canvas3D()->force_main_toolbar_left_action(canvas3D()->get_main_toolbar_item_id("layersediting")); } -void Plater::cut(size_t obj_idx, size_t instance_idx, coordf_t z, ModelObjectCutAttributes attributes) +void Plater::cut(size_t obj_idx, size_t instance_idx, const Transform3d& cut_matrix, ModelObjectCutAttributes attributes) { wxCHECK_RET(obj_idx < p->model.objects.size(), "obj_idx out of bounds"); - auto *object = p->model.objects[obj_idx]; + auto* object = p->model.objects[obj_idx]; wxCHECK_RET(instance_idx < object->instances.size(), "instance_idx out of bounds"); - if (! attributes.has(ModelObjectCutAttribute::KeepUpper) && ! attributes.has(ModelObjectCutAttribute::KeepLower)) - return; - - Plater::TakeSnapshot snapshot(this, _L("Cut by Plane")); - wxBusyCursor wait; - const auto new_objects = object->cut(instance_idx, z, attributes); - remove(obj_idx); - p->load_model_objects(new_objects); + const auto new_objects = object->cut(instance_idx, cut_matrix, attributes); + + model().delete_object(obj_idx); + sidebar().obj_list()->delete_object_from_list(obj_idx); + + // suppress to call selection update for Object List to avoid call of early Gizmos on/off update + p->load_model_objects(new_objects, false, false); + + // now process all updates of the 3d scene + update(); + // Update InfoItems in ObjectList after update() to use of a correct value of the GLCanvas3D::is_sinking(), + // which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call + for (size_t idx = 0; idx < p->model.objects.size(); idx++) + wxGetApp().obj_list()->update_info_items(idx); Selection& selection = p->get_selection(); size_t last_id = p->model.objects.size() - 1; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index d099e0361..9dc9f6316 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -242,7 +242,7 @@ public: void remove(size_t obj_idx); void reset(); void reset_with_confirm(); - void delete_object_from_model(size_t obj_idx); + bool delete_object_from_model(size_t obj_idx); void remove_selected(); void increase_instances(size_t num = 1); void decrease_instances(size_t num = 1); @@ -253,7 +253,7 @@ public: void convert_unit(ConversionType conv_type); void toggle_layers_editing(bool enable); - void cut(size_t obj_idx, size_t instance_idx, coordf_t z, ModelObjectCutAttributes attributes); + void cut(size_t obj_idx, size_t instance_idx, const Transform3d& cut_matrix, ModelObjectCutAttributes attributes); void export_gcode(bool prefer_removable); void export_stl_obj(bool extended = false, bool selection_only = false); diff --git a/src/slic3r/GUI/SavePresetDialog.cpp b/src/slic3r/GUI/SavePresetDialog.cpp index 57aa5da98..40f981722 100644 --- a/src/slic3r/GUI/SavePresetDialog.cpp +++ b/src/slic3r/GUI/SavePresetDialog.cpp @@ -163,7 +163,7 @@ void SavePresetDialog::Item::update() if (m_valid_type == ValidationType::Valid && existing) { if (m_preset_name == m_presets->get_selected_preset_name()) { - if (!rename && m_presets->get_edited_preset().is_dirty || + if ((!rename && m_presets->get_edited_preset().is_dirty) || m_parent->get_preset_bundle()) // means that we save modifications from the DiffDialog info_line = _L("Save preset modifications to existing user profile"); else diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 2423b10a0..9a4169b31 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -510,6 +510,28 @@ void Selection::volumes_changed(const std::vector &map_volume_old_to_new this->set_bounding_boxes_dirty(); } +bool Selection::is_any_connector() const +{ + const int obj_idx = get_object_idx(); + + if ((is_any_volume() || is_any_modifier() || is_mixed()) && // some solid_part AND/OR modifier is selected + obj_idx >= 0 && m_model->objects[obj_idx]->is_cut()) { + const ModelVolumePtrs& obj_volumes = m_model->objects[obj_idx]->volumes; + for (size_t vol_idx = 0; vol_idx < obj_volumes.size(); vol_idx++) + if (obj_volumes[vol_idx]->is_cut_connector()) + for (const GLVolume* v : *m_volumes) + if (v->object_idx() == obj_idx && v->volume_idx() == (int)vol_idx && v->selected) + return true; + } + return false; +} + +bool Selection::is_any_cut_volume() const +{ + const int obj_idx = get_object_idx(); + return is_any_volume() && obj_idx >= 0 && m_model->objects[obj_idx]->is_cut(); +} + bool Selection::is_single_full_instance() const { if (m_type == SingleFullInstance) diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index cebea29e0..97a85a2dc 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -320,6 +320,8 @@ public: bool is_single_volume() const { return m_type == SingleVolume; } bool is_multiple_volume() const { return m_type == MultipleVolume; } bool is_any_volume() const { return is_single_volume() || is_multiple_volume(); } + bool is_any_connector() const; + bool is_any_cut_volume() const; bool is_mixed() const { return m_type == Mixed; } bool is_from_single_instance() const { return get_instance_idx() != -1; } bool is_from_single_object() const; diff --git a/src/slic3r/GUI/UnsavedChangesDialog.hpp b/src/slic3r/GUI/UnsavedChangesDialog.hpp index 69f808451..a0b53dadf 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.hpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.hpp @@ -414,7 +414,7 @@ public: std::string get_left_preset_name(Preset::Type type); std::string get_right_preset_name(Preset::Type type); - std::vector get_selected_options(Preset::Type type) const { return std::move(m_tree->options(type, true)); } + std::vector get_selected_options(Preset::Type type) const { return m_tree->options(type, true); } protected: void on_dpi_changed(const wxRect& suggested_rect) override;