diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index c8ba1cdd31..7271af2d97 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -1553,23 +1553,11 @@ int CLI::run(int argc, char **argv) o->cut(Z, m_config.opt_float("cut"), &out); } #else - ModelObject* object = model.objects.front(); - const BoundingBoxf3& box = object->bounding_box(); - const float Margin = 20.0; - const float max_x = box.size()(0) / 2.0 + Margin; - const float min_x = -max_x; - const float max_y = box.size()(1) / 2.0 + Margin; - const float min_y = -max_y; - - std::array plane_points; - plane_points[0] = { min_x, min_y, 0 }; - plane_points[1] = { max_x, min_y, 0 }; - plane_points[2] = { max_x, max_y, 0 }; - plane_points[3] = { min_x, max_y, 0 }; - for (Vec3d& point : plane_points) { - point += box.center(); - } - model.objects.front()->cut(0, plane_points, ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::KeepLower); + Cut cut(model.objects.front(), 0, Geometry::translation_transform(m_config.opt_float("cut") * Vec3d::UnitZ()), + ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::PlaceOnCutUpper); + auto cut_objects = cut.perform_with_plane(); + for (ModelObject* obj : cut_objects) + model.add_object(*obj); #endif model.delete_object(size_t(0)); } diff --git a/src/imgui/imconfig.h b/src/imgui/imconfig.h index 9380ea1649..e9cedd8f30 100644 --- a/src/imgui/imconfig.h +++ b/src/imgui/imconfig.h @@ -153,7 +153,9 @@ namespace ImGui // const wchar_t CustomSupportsMarker = 0x1D; // const wchar_t CustomSeamMarker = 0x1E; // const wchar_t MmuSegmentationMarker = 0x1F; - + const wchar_t PlugMarker = 0x1E; + const wchar_t DowelMarker = 0x1F; + const wchar_t SnapMarker = 0x20; // Do not forget use following letters only in wstring //BBS use 08xx to avoid unicode character which may be used const wchar_t DocumentationButton = 0x0800; @@ -198,6 +200,7 @@ namespace ImGui const wchar_t CloseBlockNotifHoverButton = 0x0834; const wchar_t BlockNotifErrorIcon = 0x0835; const wchar_t ClipboardBtnDarkIcon = 0x0836; + const wchar_t InfoMarkerSmall = 0x0837; // void MyFunction(const char* name, const MyMatrix44& v); } diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index c5d6463a39..0134707178 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -202,6 +202,8 @@ set(lisbslic3r_sources BlacklistedLibraryCheck.hpp LocalesUtils.cpp LocalesUtils.hpp + CutUtils.cpp + CutUtils.hpp Model.cpp Model.hpp ModelArrange.hpp diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp new file mode 100644 index 0000000000..8dd8fd7901 --- /dev/null +++ b/src/libslic3r/CutUtils.cpp @@ -0,0 +1,663 @@ +///|/ Copyright (c) Prusa Research 2023 Oleksandra Iushchenko @YuSanka +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ + +#include "CutUtils.hpp" +#include "Geometry.hpp" +#include "libslic3r.h" +#include "Model.hpp" +#include "TriangleMeshSlicer.hpp" +#include "TriangleSelector.hpp" +#include "ObjectID.hpp" + +#include + +namespace Slic3r { + +using namespace Geometry; + +static void apply_tolerance(ModelVolume* vol) +{ + ModelVolume::CutInfo& cut_info = vol->cut_info; + + assert(cut_info.is_connector); + if (!cut_info.is_processed) + return; + + Vec3d sf = vol->get_scaling_factor(); + + // make a "hole" wider + sf[X] += double(cut_info.radius_tolerance); + sf[Y] += double(cut_info.radius_tolerance); + + // make a "hole" dipper + sf[Z] += double(cut_info.height_tolerance); + + vol->set_scaling_factor(sf); + + // correct offset in respect to the new depth + Vec3d rot_norm = rotation_transform(vol->get_rotation()) * Vec3d::UnitZ(); + if (rot_norm.norm() != 0.0) + rot_norm.normalize(); + + double z_offset = 0.5 * static_cast(cut_info.height_tolerance); + if (cut_info.connector_type == CutConnectorType::Plug || + cut_info.connector_type == CutConnectorType::Snap) + z_offset -= 0.05; // add small Z offset to better preview + + vol->set_offset(vol->get_offset() + rot_norm * z_offset); +} + +static void add_cut_volume(TriangleMesh& mesh, ModelObject* object, const ModelVolume* src_volume, const Transform3d& cut_matrix, const std::string& suffix = {}, ModelVolumeType type = ModelVolumeType::MODEL_PART) +{ + if (mesh.empty()) + return; + + mesh.transform(cut_matrix); + ModelVolume* vol = object->add_volume(mesh); + vol->set_type(type); + + vol->name = src_volume->name + suffix; + // 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; +} + +static void process_volume_cut( ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix, + ModelObjectCutAttributes attributes, TriangleMesh& upper_mesh, TriangleMesh& lower_mesh) +{ + const auto volume_matrix = volume->get_matrix(); + + const Transformation cut_transformation = Transformation(cut_matrix); + const Transform3d invert_cut_matrix = cut_transformation.get_rotation_matrix().inverse() * translation_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); + + 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); +} + +static void process_connector_cut( ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix, + ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, + std::vector& dowels) +{ + 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 (volume->cut_info.connector_type != CutConnectorType::Dowel) { + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) { + ModelVolume* vol = nullptr; + if (volume->cut_info.connector_type == CutConnectorType::Snap) { + TriangleMesh mesh = TriangleMesh(its_make_cylinder(1.0, 1.0, PI / 180.)); + + vol = upper->add_volume(std::move(mesh)); + vol->set_transformation(volume->get_transformation()); + vol->set_type(ModelVolumeType::NEGATIVE_VOLUME); + + vol->cut_info = volume->cut_info; + vol->name = volume->name; + } + else + vol = upper->add_volume(*volume); + + vol->set_transformation(volume_matrix); + apply_tolerance(vol); + } + if (attributes.has(ModelObjectCutAttribute::KeepLower)) { + ModelVolume* vol = lower->add_volume(*volume); + vol->set_transformation(volume_matrix); + // 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); + } + } + else { + if (attributes.has(ModelObjectCutAttribute::CreateDowels)) { + ModelObject* dowel{ nullptr }; + // Clone the object to duplicate instances, materials etc. + volume->get_object()->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); + + dowels.push_back(dowel); + } + + // Cut the dowel + apply_tolerance(volume); + + // Perform cut + TriangleMesh upper_mesh, lower_mesh; + process_volume_cut(volume, Transform3d::Identity(), cut_matrix, attributes, upper_mesh, lower_mesh); + + // add small Z offset to better preview + upper_mesh.translate((-0.05 * Vec3d::UnitZ()).cast()); + lower_mesh.translate((0.05 * Vec3d::UnitZ()).cast()); + + // Add cut parts to the related objects + add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A", volume->type()); + add_cut_volume(lower_mesh, lower, volume, cut_matrix, "_B", volume->type()); + } +} + +static void 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(Transformation(volume_matrix)); + + if (attributes.has(ModelObjectCutAttribute::KeepAsParts)) { + upper->add_volume(*volume); + return; + } + + // 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 process_solid_part_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix, + ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower) +{ + // Perform cut + TriangleMesh upper_mesh, lower_mesh; + process_volume_cut(volume, instance_matrix, cut_matrix, attributes, upper_mesh, lower_mesh); + + // Add required cut parts to the objects + + if (attributes.has(ModelObjectCutAttribute::KeepAsParts)) { + add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A"); + if (!lower_mesh.empty()) { + add_cut_volume(lower_mesh, upper, volume, cut_matrix, "_B"); + upper->volumes.back()->cut_info.is_from_upper = false; + } + return; + } + + 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); +} + +static void reset_instance_transformation(ModelObject* object, size_t src_instance_idx, + const Transform3d& cut_matrix = Transform3d::Identity(), + bool place_on_cut = false, bool flip = false) +{ + // 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 double rot_z = obj_instance->get_rotation().z(); + + Transformation inst_trafo = Transformation(obj_instance->get_transformation().get_matrix(false, false, true)); + // add respect to mirroring + if (obj_instance->is_left_handed()) + inst_trafo = inst_trafo * Transformation(scale_transform(Vec3d(-1, 1, 1))); + + obj_instance->set_transformation(inst_trafo); + + 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); + } +} + + +Cut::Cut(const ModelObject* object, int instance, const Transform3d& cut_matrix, + ModelObjectCutAttributes attributes/*= ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepAsParts*/) + : m_instance(instance), m_cut_matrix(cut_matrix), m_attributes(attributes) +{ + m_model = Model(); + if (object) + m_model.add_object(*object); +} + +void Cut::post_process(ModelObject* object, ModelObjectPtrs& cut_object_ptrs, bool keep, bool place_on_cut, bool flip) +{ + if (!object) return; + + if (keep && !object->volumes.empty()) { + reset_instance_transformation(object, m_instance, m_cut_matrix, place_on_cut, flip); + cut_object_ptrs.push_back(object); + } + else + m_model.objects.push_back(object); // will be deleted in m_model.clear_objects(); +} + +void Cut::post_process(ModelObject* upper, ModelObject* lower, ModelObjectPtrs& cut_object_ptrs) +{ + post_process(upper, cut_object_ptrs, + m_attributes.has(ModelObjectCutAttribute::KeepUpper), + m_attributes.has(ModelObjectCutAttribute::PlaceOnCutUpper), + m_attributes.has(ModelObjectCutAttribute::FlipUpper)); + + post_process(lower, cut_object_ptrs, + m_attributes.has(ModelObjectCutAttribute::KeepLower), + m_attributes.has(ModelObjectCutAttribute::PlaceOnCutLower), + m_attributes.has(ModelObjectCutAttribute::PlaceOnCutLower) || m_attributes.has(ModelObjectCutAttribute::FlipLower)); +} + + +void Cut::finalize(const ModelObjectPtrs& objects) +{ + //clear model from temporarry objects + m_model.clear_objects(); + + // add to model result objects + m_model.objects = objects; +} + + +const ModelObjectPtrs& Cut::perform_with_plane() +{ + if (!m_attributes.has(ModelObjectCutAttribute::KeepUpper) && !m_attributes.has(ModelObjectCutAttribute::KeepLower)) { + m_model.clear_objects(); + return m_model.objects; + } + + ModelObject* mo = m_model.objects.front(); + + BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - start"; + + // Clone the object to duplicate instances, materials etc. + ModelObject* upper{ nullptr }; + if (m_attributes.has(ModelObjectCutAttribute::KeepUpper)) + mo->clone_for_cut(&upper); + + ModelObject* lower{ nullptr }; + if (m_attributes.has(ModelObjectCutAttribute::KeepLower) && !m_attributes.has(ModelObjectCutAttribute::KeepAsParts)) + mo->clone_for_cut(&lower); + + std::vector dowels; + + // Because transformations are going to be applied to meshes directly, + // we reset transformation of all instances and volumes, + // except for translation and Z-rotation on instances, which are preserved + // in the transformation matrix and not applied to the mesh transform. + + const auto instance_matrix = mo->instances[m_instance]->get_transformation().get_matrix(true); + const Transformation cut_transformation = Transformation(m_cut_matrix); + const Transform3d inverse_cut_matrix = cut_transformation.get_rotation_matrix().inverse() * translation_transform(-1. * cut_transformation.get_offset()); + + for (ModelVolume* volume : mo->volumes) { + volume->reset_extra_facets(); + + if (!volume->is_model_part()) { + if (volume->cut_info.is_processed) + process_modifier_cut(volume, instance_matrix, inverse_cut_matrix, m_attributes, upper, lower); + else + process_connector_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower, dowels); + } + else if (!volume->mesh().empty()) + process_solid_part_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower); + } + + // Post-process cut parts + + if (m_attributes.has(ModelObjectCutAttribute::KeepAsParts) && upper->volumes.empty()) { + m_model = Model(); + m_model.objects.push_back(upper); + return m_model.objects; + } + + ModelObjectPtrs cut_object_ptrs; + + if (m_attributes.has(ModelObjectCutAttribute::KeepAsParts) && !upper->volumes.empty()) { + reset_instance_transformation(upper, m_instance, m_cut_matrix); + cut_object_ptrs.push_back(upper); + } + else { + // Delete all modifiers which are not intersecting with solid parts bounding box + auto delete_extra_modifiers = [this](ModelObject* mo) { + if (!mo) return; + const BoundingBoxf3 obj_bb = mo->instance_bounding_box(m_instance); + const Transform3d inst_matrix = mo->instances[m_instance]->get_transformation().get_matrix(); + + for (int i = int(mo->volumes.size()) - 1; i >= 0; --i) + if (const ModelVolume* vol = mo->volumes[i]; + !vol->is_model_part() && !vol->is_cut_connector()) { + auto bb = vol->mesh().transformed_bounding_box(inst_matrix * vol->get_matrix()); + if (!obj_bb.intersects(bb)) + mo->delete_volume(i); + } + }; + + post_process(upper, lower, cut_object_ptrs); + delete_extra_modifiers(upper); + delete_extra_modifiers(lower); + + if (m_attributes.has(ModelObjectCutAttribute::CreateDowels) && !dowels.empty()) { + for (auto dowel : dowels) { + reset_instance_transformation(dowel, m_instance); + dowel->name += "-Dowel-" + dowel->volumes[0]->name; + cut_object_ptrs.push_back(dowel); + } + } + } + + BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - end"; + + finalize(cut_object_ptrs); + + return m_model.objects; +} + +static void distribute_modifiers_from_object(ModelObject* from_obj, const int instance_idx, ModelObject* to_obj1, ModelObject* to_obj2) +{ + auto obj1_bb = to_obj1 ? to_obj1->instance_bounding_box(instance_idx) : BoundingBoxf3(); + auto obj2_bb = to_obj2 ? to_obj2->instance_bounding_box(instance_idx) : BoundingBoxf3(); + const Transform3d inst_matrix = from_obj->instances[instance_idx]->get_transformation().get_matrix(); + + for (ModelVolume* vol : from_obj->volumes) + if (!vol->is_model_part()) { + auto bb = vol->mesh().transformed_bounding_box(inst_matrix * vol->get_matrix()); + // Don't add modifiers which are not intersecting with solid parts + if (obj1_bb.intersects(bb)) + to_obj1->add_volume(*vol); + if (obj2_bb.intersects(bb)) + to_obj2->add_volume(*vol); + } +} + +static void merge_solid_parts_inside_object(ModelObjectPtrs& objects) +{ + for (ModelObject* mo : objects) { + TriangleMesh mesh; + // Merge all SolidPart but not Connectors + for (const ModelVolume* mv : mo->volumes) { + if (mv->is_model_part() && !mv->is_cut_connector()) { + TriangleMesh m = mv->mesh(); + m.transform(mv->get_matrix()); + mesh.merge(m); + } + } + if (!mesh.empty()) { + ModelVolume* new_volume = mo->add_volume(mesh); + new_volume->name = mo->name; + // Delete all merged SolidPart but not Connectors + for (int i = int(mo->volumes.size()) - 2; i >= 0; --i) { + const ModelVolume* mv = mo->volumes[i]; + if (mv->is_model_part() && !mv->is_cut_connector()) + mo->delete_volume(i); + } + } + } +} + + +const ModelObjectPtrs& Cut::perform_by_contour(std::vector parts, int dowels_count) +{ + ModelObject* cut_mo = m_model.objects.front(); + + // Clone the object to duplicate instances, materials etc. + ModelObject* upper{ nullptr }; + if (m_attributes.has(ModelObjectCutAttribute::KeepUpper)) cut_mo->clone_for_cut(&upper); + ModelObject* lower{ nullptr }; + if (m_attributes.has(ModelObjectCutAttribute::KeepLower)) cut_mo->clone_for_cut(&lower); + + const size_t cut_parts_cnt = parts.size(); + bool has_modifiers = false; + + // Distribute SolidParts to the Upper/Lower object + for (size_t id = 0; id < cut_parts_cnt; ++id) { + if (parts[id].is_modifier) + has_modifiers = true; // modifiers will be added later to the related parts + else if (ModelObject* obj = (parts[id].selected ? upper : lower)) + obj->add_volume(*(cut_mo->volumes[id])); + } + + if (has_modifiers) { + // Distribute Modifiers to the Upper/Lower object + distribute_modifiers_from_object(cut_mo, m_instance, upper, lower); + } + + ModelObjectPtrs cut_object_ptrs; + + ModelVolumePtrs& volumes = cut_mo->volumes; + if (volumes.size() == cut_parts_cnt) { + // Means that object is cut without connectors + + // Just add Upper and Lower objects to cut_object_ptrs + post_process(upper, lower, cut_object_ptrs); + + // Now merge all model parts together: + merge_solid_parts_inside_object(cut_object_ptrs); + + // replace initial objects in model with cut object + finalize(cut_object_ptrs); + } + else if (volumes.size() > cut_parts_cnt) { + // Means that object is cut with connectors + + // All volumes are distributed to Upper / Lower object, + // So we don’t need them anymore + for (size_t id = 0; id < cut_parts_cnt; id++) + delete* (volumes.begin() + id); + volumes.erase(volumes.begin(), volumes.begin() + cut_parts_cnt); + + // Perform cut just to get connectors + Cut cut(cut_mo, m_instance, m_cut_matrix, m_attributes); + const ModelObjectPtrs& cut_connectors_obj = cut.perform_with_plane(); + assert(dowels_count > 0 ? cut_connectors_obj.size() >= 3 : cut_connectors_obj.size() == 2); + + // Connectors from upper object + for (const ModelVolume* volume : cut_connectors_obj[0]->volumes) + upper->add_volume(*volume, volume->type()); + + // Connectors from lower object + for (const ModelVolume* volume : cut_connectors_obj[1]->volumes) + lower->add_volume(*volume, volume->type()); + + // Add Upper and Lower objects to cut_object_ptrs + post_process(upper, lower, cut_object_ptrs); + + // Now merge all model parts together: + merge_solid_parts_inside_object(cut_object_ptrs); + + // replace initial objects in model with cut object + finalize(cut_object_ptrs); + + // Add Dowel-connectors as separate objects to model + if (cut_connectors_obj.size() >= 3) + for (size_t id = 2; id < cut_connectors_obj.size(); id++) + m_model.add_object(*cut_connectors_obj[id]); + } + + return m_model.objects; +} + + +const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Transform3d& rotation_m, bool keep_as_parts/* = false*/) +{ + ModelObject* cut_mo = m_model.objects.front(); + + // Clone the object to duplicate instances, materials etc. + ModelObject* upper{ nullptr }; + cut_mo->clone_for_cut(&upper); + ModelObject* lower{ nullptr }; + cut_mo->clone_for_cut(&lower); + + const double groove_half_depth = 0.5 * double(groove.depth); + + Model tmp_model_for_cut = Model(); + + Model tmp_model = Model(); + tmp_model.add_object(*cut_mo); + ModelObject* tmp_object = tmp_model.objects.front(); + + auto add_volumes_from_cut = [](ModelObject* object, const ModelObjectCutAttribute attribute, const Model& tmp_model_for_cut) { + const auto& volumes = tmp_model_for_cut.objects.front()->volumes; + for (const ModelVolume* volume : volumes) + if (volume->is_model_part()) { + if ((attribute == ModelObjectCutAttribute::KeepUpper && volume->is_from_upper()) || + (attribute != ModelObjectCutAttribute::KeepUpper && !volume->is_from_upper())) { + ModelVolume* new_vol = object->add_volume(*volume); + new_vol->reset_from_upper(); + } + } + }; + + auto cut = [this, add_volumes_from_cut] + (ModelObject* object, const Transform3d& cut_matrix, const ModelObjectCutAttribute add_volumes_attribute, Model& tmp_model_for_cut) { + Cut cut(object, m_instance, cut_matrix); + + tmp_model_for_cut = Model(); + tmp_model_for_cut.add_object(*cut.perform_with_plane().front()); + assert(!tmp_model_for_cut.objects.empty()); + + object->clear_volumes(); + add_volumes_from_cut(object, add_volumes_attribute, tmp_model_for_cut); + reset_instance_transformation(object, m_instance); + }; + + // cut by upper plane + + const Transform3d cut_matrix_upper = translation_transform(rotation_m * (groove_half_depth * Vec3d::UnitZ())) * m_cut_matrix; + { + cut(tmp_object, cut_matrix_upper, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut); + add_volumes_from_cut(upper, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut); + } + + // cut by lower plane + + const Transform3d cut_matrix_lower = translation_transform(rotation_m * (-groove_half_depth * Vec3d::UnitZ())) * m_cut_matrix; + { + cut(tmp_object, cut_matrix_lower, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut); + add_volumes_from_cut(lower, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut); + } + + // cut middle part with 2 angles and add parts to related upper/lower objects + + const double h_side_shift = 0.5 * double(groove.width + groove.depth / tan(groove.flaps_angle)); + + // cut by angle1 plane + { + const Transform3d cut_matrix_angle1 = translation_transform(rotation_m * (-h_side_shift * Vec3d::UnitX())) * m_cut_matrix * rotation_transform(Vec3d(0, -groove.flaps_angle, -groove.angle)); + + cut(tmp_object, cut_matrix_angle1, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut); + add_volumes_from_cut(lower, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut); + } + + // cut by angle2 plane + { + const Transform3d cut_matrix_angle2 = translation_transform(rotation_m * (h_side_shift * Vec3d::UnitX())) * m_cut_matrix * rotation_transform(Vec3d(0, groove.flaps_angle, groove.angle)); + + cut(tmp_object, cut_matrix_angle2, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut); + add_volumes_from_cut(lower, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut); + } + + // apply tolerance to the middle part + { + const double h_groove_shift_tolerance = groove_half_depth - (double)groove.depth_tolerance; + + const Transform3d cut_matrix_lower_tolerance = translation_transform(rotation_m * (-h_groove_shift_tolerance * Vec3d::UnitZ())) * m_cut_matrix; + cut(tmp_object, cut_matrix_lower_tolerance, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut); + + const double h_side_shift_tolerance = h_side_shift - 0.5 * double(groove.width_tolerance); + + const Transform3d cut_matrix_angle1_tolerance = translation_transform(rotation_m * (-h_side_shift_tolerance * Vec3d::UnitX())) * m_cut_matrix * rotation_transform(Vec3d(0, -groove.flaps_angle, -groove.angle)); + cut(tmp_object, cut_matrix_angle1_tolerance, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut); + + const Transform3d cut_matrix_angle2_tolerance = translation_transform(rotation_m * (h_side_shift_tolerance * Vec3d::UnitX())) * m_cut_matrix * rotation_transform(Vec3d(0, groove.flaps_angle, groove.angle)); + cut(tmp_object, cut_matrix_angle2_tolerance, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut); + } + + // this part can be added to the upper object now + add_volumes_from_cut(upper, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut); + + ModelObjectPtrs cut_object_ptrs; + + if (keep_as_parts) { + // add volumes from lower object to the upper, but mark them as a lower + const auto& volumes = lower->volumes; + for (const ModelVolume* volume : volumes) { + ModelVolume* new_vol = upper->add_volume(*volume); + new_vol->cut_info.is_from_upper = false; + } + + // add modifiers + for (const ModelVolume* volume : cut_mo->volumes) + if (!volume->is_model_part()) + upper->add_volume(*volume); + + cut_object_ptrs.push_back(upper); + + // add lower object to the cut_object_ptrs just to correct delete it from the Model destructor and avoid memory leaks + cut_object_ptrs.push_back(lower); + } + else { + // add modifiers if object has any + for (const ModelVolume* volume : cut_mo->volumes) + if (!volume->is_model_part()) { + distribute_modifiers_from_object(cut_mo, m_instance, upper, lower); + break; + } + + assert(!upper->volumes.empty() && !lower->volumes.empty()); + + // Add Upper and Lower parts to cut_object_ptrs + + post_process(upper, lower, cut_object_ptrs); + + // Now merge all model parts together: + merge_solid_parts_inside_object(cut_object_ptrs); + } + + finalize(cut_object_ptrs); + + return m_model.objects; +} + +ModelObjectPtrs Cut::cut_horizontal(const ModelObject *object, size_t instance_idx, double z, ModelObjectCutAttributes attributes) +{ + Cut cut(object, instance_idx, Geometry::translation_transform(z * Vec3d::UnitZ()), attributes); + return cut.perform_with_plane(); +} + +} // namespace Slic3r + diff --git a/src/libslic3r/CutUtils.hpp b/src/libslic3r/CutUtils.hpp new file mode 100644 index 0000000000..d2bf88db54 --- /dev/null +++ b/src/libslic3r/CutUtils.hpp @@ -0,0 +1,74 @@ +///|/ Copyright (c) Prusa Research 2023 Oleksandra Iushchenko @YuSanka +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef slic3r_CutUtils_hpp_ +#define slic3r_CutUtils_hpp_ + +#include "enum_bitmask.hpp" +#include "Point.hpp" +#include "Model.hpp" + +#include + +namespace Slic3r { + +using ModelObjectPtrs = std::vector; + +enum class ModelObjectCutAttribute : int { KeepUpper, KeepLower, KeepAsParts, FlipUpper, FlipLower, PlaceOnCutUpper, PlaceOnCutLower, CreateDowels, InvalidateCutInfo }; +using ModelObjectCutAttributes = enum_bitmask; +ENABLE_ENUM_BITMASK_OPERATORS(ModelObjectCutAttribute); + + +class Cut { + + Model m_model; + int m_instance; + const Transform3d m_cut_matrix; + ModelObjectCutAttributes m_attributes; + + void post_process(ModelObject* object, ModelObjectPtrs& objects, bool keep, bool place_on_cut, bool flip); + void post_process(ModelObject* upper_object, ModelObject* lower_object, ModelObjectPtrs& objects); + void finalize(const ModelObjectPtrs& objects); + +public: + + Cut(const ModelObject* object, int instance, const Transform3d& cut_matrix, + ModelObjectCutAttributes attributes = ModelObjectCutAttribute::KeepUpper | + ModelObjectCutAttribute::KeepLower | + ModelObjectCutAttribute::KeepAsParts ); + ~Cut() { m_model.clear_objects(); } + + struct Groove + { + float depth{ 0.f }; + float width{ 0.f }; + float flaps_angle{ 0.f }; + float angle{ 0.f }; + float depth_init{ 0.f }; + float width_init{ 0.f }; + float flaps_angle_init{ 0.f }; + float angle_init{ 0.f }; + float depth_tolerance{ 0.1f }; + float width_tolerance{ 0.1f }; + }; + + struct Part + { + bool selected; + bool is_modifier; + }; + + const ModelObjectPtrs& perform_with_plane(); + const ModelObjectPtrs& perform_by_contour(std::vector parts, int dowels_count); + const ModelObjectPtrs& perform_with_groove(const Groove& groove, const Transform3d& rotation_m, bool keep_as_parts = false); + + static ModelObjectPtrs cut_horizontal(const ModelObject *object, size_t instance_idx, double z, ModelObjectCutAttributes attributes); + +}; // namespace Cut + + + +} // namespace Slic3r + +#endif /* slic3r_CutUtils_hpp_ */ diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index f6936efaaa..c616d0f1b4 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -1,3 +1,8 @@ +///|/ Copyright (c) Prusa Research 2018 - 2023 Oleksandra Iushchenko @YuSanka, David Kocík @kocikdav, Enrico Turri @enricoturri1966, Lukáš Matěna @lukasmatena, Lukáš Hejl @hejllukas, Filip Sykala @Jony01, Vojtěch Bubník @bubnikv, Tomáš Mészáros @tamasmeszaros +///|/ Copyright (c) 2020 Henner Zeller +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "../libslic3r.h" #include "../Exception.hpp" #include "../Model.hpp" @@ -741,8 +746,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) { int volume_id; int type; - float radius; - float height; float r_tolerance; float h_tolerance; }; @@ -758,10 +761,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) //typedef std::map IdToAliasesMap; typedef std::vector InstancesList; typedef std::map IdToMetadataMap; - typedef std::map IdToCutObjectInfoMap; //typedef std::map IdToGeometryMap; typedef std::map> IdToLayerHeightsProfileMap; typedef std::map IdToLayerConfigRangesMap; + typedef std::map IdToCutObjectInfoMap; /*typedef std::map> IdToSlaSupportPointsMap; typedef std::map> IdToSlaDrainHolesMap;*/ @@ -951,7 +954,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) //IdToGeometryMap m_orig_geometries; // backup & restore CurrentConfig m_curr_config; IdToMetadataMap m_objects_metadata; - IdToCutObjectInfoMap m_cut_object_infos; + IdToCutObjectInfoMap m_cut_object_infos; IdToLayerHeightsProfileMap m_layer_heights_profiles; IdToLayerConfigRangesMap m_layer_config_ranges; /*IdToSlaSupportPointsMap m_sla_support_points; @@ -1013,7 +1016,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool _extract_xml_from_archive(mz_zip_archive& archive, std::string const & path, XML_StartElementHandler start_handler, XML_EndElementHandler end_handler); bool _extract_xml_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, XML_StartElementHandler start_handler, XML_EndElementHandler end_handler); 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_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); @@ -1955,11 +1958,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) 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; - + int vol_cnt = int(model_object->volumes.size()); 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.radius, connector.height, connector.r_tolerance, connector.h_tolerance, true); + if (connector.volume_id < 0 || connector.volume_id >= vol_cnt) { + add_error("Invalid connector is found"); + continue; + } + model_object->volumes[connector.volume_id]->cut_info = + ModelVolume::CutInfo(CutConnectorType(connector.type), connector.r_tolerance, connector.h_tolerance, true); } } } @@ -2324,7 +2330,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) void _BBS_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); + 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"); @@ -2332,12 +2338,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) } std::istringstream iss(buffer); // wrap returned xml to istringstream - pt::ptree objects_tree; + pt::ptree objects_tree; pt::read_xml(iss, objects_tree); - for (const auto &object : objects_tree.get_child("objects")) { + for (const auto& object : objects_tree.get_child("objects")) { pt::ptree object_tree = object.second; - int obj_idx = object_tree.get(".id", -1); + int obj_idx = object_tree.get(".id", -1); if (obj_idx <= 0) { add_error("Found invalid object id"); continue; @@ -2349,30 +2355,33 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) continue; } - CutObjectBase cut_id; - std::vector connectors; + CutObjectBase cut_id; + std::vector connectors; - for (const auto &obj_cut_info : object_tree) { + 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")); + 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(".radius", 0.f), connector_tree.get(".height", 0.f), - connector_tree.get(".r_tolerance"), connector_tree.get(".h_tolerance")}; + 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}); + CutObjectInfo cut_info {cut_id, connectors}; + m_cut_object_infos.insert({ obj_idx, cut_info }); } } } @@ -5260,6 +5269,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) //BBS: change volume to seperate objects bool _add_mesh_to_object_stream(std::function const &flush, ObjectData const &object_data) const; bool _add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items) const; + 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); @@ -5270,7 +5280,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) //BBS: add project embedded preset files bool _add_project_embedded_presets_to_archive(mz_zip_archive& archive, Model& model, std::vector project_presets); bool _add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, int export_plate_idx = -1, bool save_gcode = true, bool use_loaded_id = false); - bool _add_cut_information_file_to_archive(mz_zip_archive &archive, Model &model); bool _add_slice_info_config_file_to_archive(mz_zip_archive &archive, const Model &model, PlateDataPtrs &plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config); bool _add_gcode_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, Export3mfProgressFn proFn = nullptr); bool _add_custom_gcode_per_print_z_file_to_archive(mz_zip_archive& archive, Model& model, const DynamicPrintConfig* config); @@ -6704,6 +6713,69 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } + bool _BBS_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++; + if (!object->is_cut()) + continue; + 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 _BBS_3MF_Exporter::_add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model) { assert(is_decimal_separator_point()); @@ -7266,69 +7338,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } - bool _BBS_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(".radius", volume->cut_info.radius); - connectors_tree.put(".height", volume->cut_info.height); - 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 _BBS_3MF_Exporter::_add_slice_info_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config) { std::stringstream stream; diff --git a/src/libslic3r/Geometry.cpp b/src/libslic3r/Geometry.cpp index ecee432bba..e57b774f34 100644 --- a/src/libslic3r/Geometry.cpp +++ b/src/libslic3r/Geometry.cpp @@ -1,3 +1,17 @@ +///|/ Copyright (c) Prusa Research 2016 - 2023 Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966, Lukáš Matěna @lukasmatena, Filip Sykala @Jony01, Tomáš Mészáros @tamasmeszaros +///|/ Copyright (c) Slic3r 2013 - 2016 Alessandro Ranellucci @alranel +///|/ +///|/ ported from lib/Slic3r/Geometry.pm: +///|/ Copyright (c) Prusa Research 2017 - 2022 Vojtěch Bubník @bubnikv +///|/ Copyright (c) Slic3r 2011 - 2015 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2013 Jose Luis Perez Diez +///|/ Copyright (c) 2013 Anders Sundman +///|/ Copyright (c) 2013 Jesse Vincent +///|/ Copyright (c) 2012 Mike Sheldrake @mesheldrake +///|/ Copyright (c) 2012 Mark Hindess +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "libslic3r.h" #include "Exception.hpp" #include "Geometry.hpp" @@ -432,6 +446,14 @@ Vec3d extract_euler_angles(const Transform3d& transform) return extract_euler_angles(m); } +static Transform3d extract_rotation_matrix(const Transform3d& trafo) +{ + Matrix3d rotation; + Matrix3d scale; + trafo.computeRotationScaling(&rotation, &scale); + return Transform3d(rotation); +} + void rotation_from_two_vectors(Vec3d from, Vec3d to, Vec3d& rotation_axis, double& phi, Matrix3d* rotation_matrix) { double epsilon = 1e-5; @@ -504,6 +526,11 @@ void Transformation::set_offset(Axis axis, double offset) } } +Transform3d Transformation::get_rotation_matrix() const +{ + return extract_rotation_matrix(m_matrix); +} + void Transformation::set_rotation(const Vec3d& rotation) { set_rotation(X, rotation.x()); diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp index 0a3dcad7d5..4f6511ff6f 100644 --- a/src/libslic3r/Geometry.hpp +++ b/src/libslic3r/Geometry.hpp @@ -440,6 +440,8 @@ public: const Vec3d& get_rotation() const { return m_rotation; } double get_rotation(Axis axis) const { return m_rotation(axis); } + Transform3d get_rotation_matrix() const; + void set_rotation(const Vec3d& rotation); void set_rotation(Axis axis, double rotation); diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 90879d49c2..2d0ff44ca3 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1,3 +1,16 @@ +///|/ Copyright (c) Prusa Research 2016 - 2023 Tomáš Mészáros @tamasmeszaros, Oleksandra Iushchenko @YuSanka, David Kocík @kocikdav, Enrico Turri @enricoturri1966, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv, Lukáš Hejl @hejllukas, Filip Sykala @Jony01, Vojtěch Král @vojtechkral +///|/ Copyright (c) 2021 Boleslaw Ciesielski +///|/ Copyright (c) 2019 John Drake @foxox +///|/ Copyright (c) 2019 Sijmen Schoon +///|/ Copyright (c) Slic3r 2014 - 2016 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2015 Maksim Derbasov @ntfshard +///|/ +///|/ ported from lib/Slic3r/Model.pm: +///|/ Copyright (c) Prusa Research 2016 - 2022 Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966 +///|/ Copyright (c) Slic3r 2012 - 2016 Alessandro Ranellucci @alranel +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "Model.hpp" #include "libslic3r.h" #include "BuildVolume.hpp" @@ -1693,68 +1706,6 @@ bool ModelObject::has_connectors() const 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 &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); - - Transform3d translate_transform = Transform3d::Identity(); - translate_transform.translate(connector.pos); - Transform3d scale_transform = Transform3d::Identity(); - scale_transform.scale(Vec3f(connector.radius, connector.radius, connector.height).cast()); - - // Transform the new modifier to be aligned inside the instance - new_volume->set_transformation(translate_transform * connector.rotation_m * scale_transform); - - new_volume->cut_info = {connector.attribs.type, connector.radius, connector.height, connector.radius_tolerance, connector.height_tolerance}; - new_volume->name = 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(); @@ -1770,43 +1721,10 @@ void ModelObject::delete_connectors() } } -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) || - attributes.has(ModelObjectCutAttribute::InvalidateCutInfo)) - 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)->set_model(this->get_model()); (*obj)->sla_support_points.clear(); (*obj)->sla_drain_holes.clear(); (*obj)->sla_points_status = sla::PointsStatus::NoPoints; @@ -1814,189 +1732,11 @@ void ModelObject::clone_for_cut(ModelObject **obj) (*obj)->input_file.clear(); } -Transform3d ModelObject::calculate_cut_plane_inverse_matrix(const std::array& plane_points) +void ModelVolume::reset_extra_facets() { - Vec3d mid_point = {0.0, 0.0, 0.0}; - for (auto pt : plane_points) - mid_point += pt; - mid_point /= (double) plane_points.size(); - - Vec3d movement = -mid_point; - - Vec3d v01 = plane_points[1] - plane_points[0]; - Vec3d v12 = plane_points[2] - plane_points[1]; - - Vec3d plane_normal = v01.cross(v12); - plane_normal.normalize(); - - Vec3d axis = {0.0, 0.0, 0.0}; - double phi = 0.0; - Matrix3d matrix; - matrix.setIdentity(); - Geometry::rotation_from_two_vectors(plane_normal, {0.0, 0.0, 1.0}, axis, phi, &matrix); - Vec3d angles = Geometry::extract_euler_angles(matrix); - - movement = matrix * movement; - Transform3d transfo; - transfo.setIdentity(); - transfo.translate(movement); - transfo.rotate(Eigen::AngleAxisd(angles(2), Vec3d::UnitZ()) * Eigen::AngleAxisd(angles(1), Vec3d::UnitY()) * Eigen::AngleAxisd(angles(0), Vec3d::UnitX())); - return transfo; -} - -void ModelObject::process_connector_cut( - ModelVolume *volume, - const Transform3d & instance_matrix, - const Transform3d& cut_matrix, - 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 (volume->cut_info.connector_type != CutConnectorType::Dowel) { - 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); - // 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); - } - } - else { - if (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); - } - - // Cut the dowel - volume->apply_tolerance(); - - // Perform cut - TriangleMesh upper_mesh, lower_mesh; - process_volume_cut(volume, Transform3d::Identity(), cut_matrix, attributes, upper_mesh, lower_mesh); - - // add small Z offset to better preview - upper_mesh.translate((-0.05 * Vec3d::UnitZ()).cast()); - lower_mesh.translate((0.05 * Vec3d::UnitZ()).cast()); - - // Add cut parts to the related objects - add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A", volume->type()); - add_cut_volume(lower_mesh, lower, volume, cut_matrix, "_B", volume->type()); - } -} - -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)); - - if (attributes.has(ModelObjectCutAttribute::CutToParts)) { - upper->add_volume(*volume); - return; - } - - // 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); -} - -void ModelObject::process_volume_cut(ModelVolume * volume, - const Transform3d & instance_matrix, - const Transform3d & cut_matrix, - ModelObjectCutAttributes attributes, - TriangleMesh & upper_mesh, - TriangleMesh & lower_mesh) -{ - const auto volume_matrix = volume->get_matrix(); - - using namespace Geometry; - - const Geometry::Transformation cut_transformation = Geometry::Transformation(cut_matrix); - const Transform3d invert_cut_matrix = cut_transformation.get_matrix(true, false, true, true).inverse() - * translation_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); - - 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); -} - -void ModelObject::process_solid_part_cut(ModelVolume * volume, - const Transform3d & instance_matrix, - const Transform3d & cut_matrix, - const std::array &plane_points, - ModelObjectCutAttributes attributes, - ModelObject * upper, - ModelObject * lower, - Vec3d & local_displace) -{ - // Perform cut - TriangleMesh upper_mesh, lower_mesh; - process_volume_cut(volume, instance_matrix, cut_matrix, attributes, upper_mesh, lower_mesh); - - // Add required cut parts to the objects - if (attributes.has(ModelObjectCutAttribute::CutToParts)) { - add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A"); - add_cut_volume(lower_mesh, upper, volume, cut_matrix, "_B"); - return; - } - - 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)); - } + this->supported_facets.reset(); + this->seam_facets.reset(); + this->mmu_segmentation_facets.reset(); } static void invalidate_translations(ModelObject* object, const ModelInstance* src_instance) @@ -2073,215 +1813,6 @@ static void reset_instance_transformation(ModelObject* object, size_t src_instan } } -// BBS: replace z with plane_points -ModelObjectPtrs ModelObject::cut(size_t instance, std::array plane_points, 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); - - ModelObject* upper{ nullptr }; - if (attributes.has(ModelObjectCutAttribute::KeepUpper)) - clone_for_cut(&upper); - - ModelObject* lower{ nullptr }; - if (attributes.has(ModelObjectCutAttribute::KeepLower) && !attributes.has(ModelObjectCutAttribute::CutToParts)) - clone_for_cut(&lower); - - // Because transformations are going to be applied to meshes directly, - // we reset transformation of all instances and volumes, - // except for translation and Z-rotation on instances, which are preserved - // 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( - Vec3d::Zero(), // don't apply offset - instances[instance]->get_rotation().cwiseProduct(Vec3d(1.0, 1.0, 1.0)), // BBS: do apply Z-rotation - instances[instance]->get_scaling_factor(), - instances[instance]->get_mirror() - ); - - // BBS - //z -= instances[instance]->get_offset().z(); - for (Vec3d& point : plane_points) { - point -= instances[instance]->get_offset(); - } - Transform3d inverse_cut_matrix = calculate_cut_plane_inverse_matrix(plane_points); - Transform3d cut_matrix = inverse_cut_matrix.inverse(); - - std::vector dowels; - // 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(); - - volume->supported_facets.reset(); - volume->seam_facets.reset(); - volume->mmu_segmentation_facets.reset(); - - if (! volume->is_model_part()) { - if (volume->cut_info.is_processed) { - // Modifiers are not cut, but we still need to add the instance transformation - // to the modifier volume transformation to preserve their shape properly. - //Transform3d inverse_cut_matrix = calculate_cut_plane_inverse_matrix(plane_points); - process_modifier_cut(volume, instance_matrix, inverse_cut_matrix, attributes, upper, lower); - } - else { - process_connector_cut(volume, instance_matrix, cut_matrix, attributes, upper, lower, dowels, local_dowels_displace); - } - } - else if (! volume->mesh().empty()) { - process_solid_part_cut(volume, instance_matrix, cut_matrix, plane_points, attributes, upper, lower, local_displace); - } - } - - ModelObjectPtrs res; - - if (attributes.has(ModelObjectCutAttribute::CutToParts) && !upper->volumes.empty()) { - reset_instance_transformation(upper, instance, cut_matrix); - res.push_back(upper); - } - else { - if (attributes.has(ModelObjectCutAttribute::KeepUpper) && upper->volumes.size() > 0) { - 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) { - 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) { - 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; -} - -// BBS -ModelObjectPtrs ModelObject::segment(size_t instance, unsigned int max_extruders, double smoothing_alpha, int segment_number) -{ - BOOST_LOG_TRIVIAL(trace) << "ModelObject::segment - start"; - - // Clone the object to duplicate instances, materials etc. - ModelObject* upper = ModelObject::new_clone(*this); - - 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(); - - // Because transformations are going to be applied to meshes directly, - // we reset transformation of all instances and volumes, - // except for translation and Z-rotation on instances, which are preserved - // 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( - Vec3d::Zero(), // don't apply offset - instances[instance]->get_rotation(), // BBS: keep Z-rotation - instances[instance]->get_scaling_factor(), - instances[instance]->get_mirror() - ); - - for (ModelVolume* volume : volumes) { - const auto volume_matrix = volume->get_matrix(); - - volume->supported_facets.reset(); - volume->seam_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)); - upper->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(); - - auto mesh_segments = MeshBoolean::cgal::segment(mesh, smoothing_alpha, segment_number); - - - // Reset volume transformation except for offset - const Vec3d offset = volume->get_offset(); - volume->set_transformation(Geometry::Transformation()); - volume->set_offset(offset); - - unsigned int extruder_counter = 0; - for (int idx=0;idx 0) { - ModelVolume* vol = upper->add_volume(mesh_segment); - vol->name = volume->name.substr(0, volume->name.find_last_of('.')) + "_" + std::to_string(idx); - // Don't copy the config's ID. - vol->config.assign_config(volume->config); -#if 0 - assert(vol->config.id().valid()); - assert(vol->config.id() != volume->config.id()); - vol->set_material(volume->material_id(), *volume->material()); -#else - vol->config.set("extruder", auto_extruder_id(max_extruders, extruder_counter)); -#endif - } - } - } - } - - ModelObjectPtrs res; - - if (upper->volumes.size() > 0) { - upper->invalidate_bounding_box(); - - // 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(); - // BBS - //const double rot_z = instance->get_rotation()(2); - - instance->set_transformation(Geometry::Transformation()); - instance->set_offset(offset); - // BBS - //instance->set_rotation(Vec3d(0.0, 0.0, rot_z)); - } - - res.push_back(upper); - } - - BOOST_LOG_TRIVIAL(trace) << "ModelObject::segment - end"; - - return res; -} - void ModelObject::split(ModelObjectPtrs* new_objects) { std::vector all_meshes; @@ -2801,35 +2332,6 @@ bool ModelVolume::is_splittable() const return m_is_splittable == 1; } -void ModelVolume::apply_tolerance() -{ - assert(cut_info.is_connector); - if (!cut_info.is_processed) - return; - - Vec3d sf = get_scaling_factor(); - // make a "hole" wider - double size_scale = 1.f; - if (abs(cut_info.radius - 0) < EPSILON) // For compatibility with old files - size_scale = 1.f + double(cut_info.radius_tolerance); - else - size_scale = (double(cut_info.radius) + double(cut_info.radius_tolerance)) / double(cut_info.radius); - - sf[X] *= size_scale; - sf[Y] *= size_scale; - - // make a "hole" dipper - double height_scale = 1.f; - if (abs(cut_info.height - 0) < EPSILON) // For compatibility with old files - height_scale = 1.f + double(cut_info.height_tolerance); - else - height_scale = (double(cut_info.height) + double(cut_info.height_tolerance)) / double(cut_info.height); - - sf[Z] *= height_scale; - - set_scaling_factor(sf); -} - // BBS std::vector ModelVolume::get_extruders() const { diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 33e7ff6a90..0705012c76 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -246,79 +246,92 @@ private: }; enum class CutConnectorType : int { - Plug, - Dowel, - Undef + Plug + , Dowel + , Snap + , Undef }; enum class CutConnectorStyle : int { - Prizm, - Frustum, - Undef + Prism + , Frustum + , Undef //,Claw }; enum class CutConnectorShape : int { - Triangle, - Square, - Hexagon, - Circle, - Undef + Triangle + , Square + , Hexagon + , Circle + , Undef //,D-shape }; struct CutConnectorAttributes { - CutConnectorType type{CutConnectorType::Plug}; - CutConnectorStyle style{CutConnectorStyle::Prizm}; - CutConnectorShape shape{CutConnectorShape::Circle}; + CutConnectorType type{ CutConnectorType::Plug }; + CutConnectorStyle style{ CutConnectorStyle::Prism }; + CutConnectorShape shape{ CutConnectorShape::Circle }; CutConnectorAttributes() {} - CutConnectorAttributes(CutConnectorType t, CutConnectorStyle st, CutConnectorShape sh) : type(t), style(st), shape(sh) {} + CutConnectorAttributes(CutConnectorType t, CutConnectorStyle st, CutConnectorShape sh) + : type(t), style(st), shape(sh) + {} - CutConnectorAttributes(const CutConnectorAttributes &rhs) : CutConnectorAttributes(rhs.type, rhs.style, rhs.shape) {} + CutConnectorAttributes(const CutConnectorAttributes& rhs) : + CutConnectorAttributes(rhs.type, rhs.style, rhs.shape) {} - bool operator==(const CutConnectorAttributes &other) const; + bool operator==(const CutConnectorAttributes& other) const; - bool operator!=(const CutConnectorAttributes &other) const { return !(other == (*this)); } + 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); + 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); } + 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] + Vec3d pos; + Transform3d rotation_m; + float radius; + float height; + float radius_tolerance;// [0.f : 1.f] + float height_tolerance;// [0.f : 1.f] + float z_angle {0.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() + : pos(Vec3d::Zero()), rotation_m(Transform3d::Identity()), radius(5.f), height(10.f), radius_tolerance(0.f), height_tolerance(0.1f), z_angle(0.f) {} - CutConnector(const CutConnector &rhs) : CutConnector(rhs.pos, rhs.rotation_m, rhs.radius, rhs.height, rhs.radius_tolerance, rhs.height_tolerance, rhs.attribs) {} + CutConnector(Vec3d p, Transform3d rot, float r, float h, float rt, float ht, float za, CutConnectorAttributes attributes) + : pos(p), rotation_m(rot), radius(r), height(h), radius_tolerance(rt), height_tolerance(ht), z_angle(za), attribs(attributes) + {} - bool operator==(const CutConnector &other) const; + CutConnector(const CutConnector& rhs) : + CutConnector(rhs.pos, rhs.rotation_m, rhs.radius, rhs.height, rhs.radius_tolerance, rhs.height_tolerance, rhs.z_angle, rhs.attribs) {} - bool operator!=(const CutConnector &other) const { return !(other == (*this)); } + bool operator==(const CutConnector& other) const; - template inline void serialize(Archive &ar) { ar(pos, rotation_m, radius, height, radius_tolerance, height_tolerance, attribs); } + 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, z_angle, attribs); + } }; using CutConnectors = std::vector; + // Declared outside of ModelVolume, so it could be forward declared. enum class ModelVolumeType : int { INVALID = -1, @@ -326,13 +339,9 @@ enum class ModelVolumeType : int { NEGATIVE_VOLUME, PARAMETER_MODIFIER, SUPPORT_BLOCKER, - SUPPORT_ENFORCER + SUPPORT_ENFORCER, }; -enum class ModelObjectCutAttribute : int { KeepUpper, KeepLower, FlipUpper, FlipLower, PlaceOnCutUpper, PlaceOnCutLower, CreateDowels, CutToParts, InvalidateCutInfo }; -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, @@ -371,6 +380,10 @@ public: // 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 @@ -380,10 +393,6 @@ public: // BBS: save for compare with new load volumes std::vector volume_ids; - // 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; - Model* get_model() { return m_model; } const Model* get_model() const { return m_model; } // BBS: production extension @@ -480,52 +489,13 @@ public: size_t materials_count() const; size_t facets_count() const; size_t parts_count() const; - - bool is_cut() const { return cut_id.id().valid(); } - bool has_connectors() 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(); // delete volumes which are marked as connector for this object void delete_connectors(); - void synchronize_model_after_cut(); - void apply_cut_attributes(ModelObjectCutAttributes attributes); void clone_for_cut(ModelObject **obj); - Transform3d calculate_cut_plane_inverse_matrix(const std::array &plane_points); - void process_connector_cut(ModelVolume *volume, - const Transform3d & instance_matrix, - const Transform3d& cut_matrix, - 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_volume_cut(ModelVolume * volume, - const Transform3d & instance_matrix, - const Transform3d & cut_matrix, - ModelObjectCutAttributes attributes, - TriangleMesh & upper_mesh, - TriangleMesh & lower_mesh); - void process_solid_part_cut(ModelVolume * volume, - const Transform3d & instance_matrix, - const Transform3d & cut_matrix, - const std::array &plane_points, - ModelObjectCutAttributes attributes, - ModelObject * upper, - ModelObject * lower, - Vec3d & local_displace); - // BBS: replace z with plane_points - ModelObjectPtrs cut(size_t instance, std::array plane_points, ModelObjectCutAttributes attributes); - // BBS - ModelObjectPtrs segment(size_t instance, unsigned int max_extruders, double smoothing_alpha = 0.5, int segment_number = 5); - void split(ModelObjectPtrs* new_objects); + void split(ModelObjectPtrs*new_objects); void merge(); // BBS: Boolean opts - Musang King @@ -553,6 +523,8 @@ public: // 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. @@ -853,33 +825,41 @@ public: }; Source source; - // struct used by cut command + // 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{0.f}; - float height{0.f}; - float radius_tolerance{0.f}; // [0.f : 1.f] - float height_tolerance{0.f}; // [0.f : 1.f] + bool is_from_upper{ true }; + 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 radius_, float height_, float rad_tolerance, float h_tolerance, bool processed = false) - : is_connector(true), is_processed(processed), connector_type(type) - , radius(radius_), height(height_), radius_tolerance(rad_tolerance), height_tolerance(h_tolerance) + 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; } + void invalidate() { is_connector = false; } + void reset_from_upper() { is_from_upper = true; } - template inline void serialize(Archive &ar) { ar(is_connector, is_processed, connector_type, radius_tolerance, height_tolerance); } + template inline void serialize(Archive& ar) { + ar(is_connector, is_processed, connector_type, radius_tolerance, height_tolerance); + } }; - CutInfo cut_info; + CutInfo cut_info; - bool is_cut_connector() const { return cut_info.is_processed && cut_info.is_connector; } - void invalidate_cut_info() { cut_info.invalidate(); } + bool is_from_upper() const { return cut_info.is_from_upper; } + void reset_from_upper() { cut_info.reset_from_upper(); } + + 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(); } @@ -922,6 +902,7 @@ public: 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 set_material_id(t_model_material_id material_id); ModelMaterial* material() const; void set_material(t_model_material_id material_id, const ModelMaterial &material); @@ -931,8 +912,6 @@ public: bool is_splittable() const; - void apply_tolerance(); - // BBS std::vector get_extruders() const; void update_extruder_count(size_t extruder_count); diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index b5d0d10883..92557dfdb5 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -1,3 +1,18 @@ +///|/ Copyright (c) Prusa Research 2016 - 2023 Oleksandra Iushchenko @YuSanka, Enrico Turri @enricoturri1966, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv, Tomáš Mészáros @tamasmeszaros, Filip Sykala @Jony01, Lukáš Hejl @hejllukas, Vojtěch Král @vojtechkral +///|/ Copyright (c) 2019 Jason Tibbitts @jasontibbitts +///|/ Copyright (c) 2019 Sijmen Schoon +///|/ Copyright (c) 2016 Joseph Lenox @lordofhyphens +///|/ Copyright (c) Slic3r 2013 - 2016 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2015 Maksim Derbasov @ntfshard +///|/ Copyright (c) 2014 Miro Hrončok @hroncok +///|/ Copyright (c) 2014 Petr Ledvina @ledvinap +///|/ +///|/ ported from lib/Slic3r/TriangleMesh.pm: +///|/ Copyright (c) Slic3r 2011 - 2014 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2012 - 2013 Mark Hindess +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "Exception.hpp" #include "TriangleMesh.hpp" #include "TriangleMeshSlicer.hpp" @@ -958,6 +973,51 @@ indexed_triangle_set its_make_cylinder(double r, double h, double fa) return mesh; } +indexed_triangle_set its_make_frustum(double r, double h, double fa) +{ + indexed_triangle_set mesh; + size_t n_steps = (size_t)ceil(2. * PI / fa); + double angle_step = 2. * PI / n_steps; + + auto &vertices = mesh.vertices; + auto &facets = mesh.indices; + vertices.reserve(2 * n_steps + 2); + facets.reserve(4 * n_steps); + + // 2 special vertices, top and bottom center, rest are relative to this + vertices.emplace_back(Vec3f(0.f, 0.f, 0.f)); + vertices.emplace_back(Vec3f(0.f, 0.f, float(h))); + + // for each line along the polygon approximating the top/bottom of the + // circle, generate four points and four facets (2 for the wall, 2 for the + // top and bottom. + // Special case: Last line shares 2 vertices with the first line. + Vec2f vec_top = Eigen::Rotation2Df(0.f) * Eigen::Vector2f(0, 0.5f*r); + Vec2f vec_botton = Eigen::Rotation2Df(0.f) * Eigen::Vector2f(0, r); + + vertices.emplace_back(Vec3f(vec_botton(0), vec_botton(1), 0.f)); + vertices.emplace_back(Vec3f(vec_top(0), vec_top(1), float(h))); + for (size_t i = 1; i < n_steps; ++i) { + vec_top = Eigen::Rotation2Df(angle_step * i) * Eigen::Vector2f(0, 0.5f*float(r)); + vec_botton = Eigen::Rotation2Df(angle_step * i) * Eigen::Vector2f(0, float(r)); + vertices.emplace_back(Vec3f(vec_botton(0), vec_botton(1), 0.f)); + vertices.emplace_back(Vec3f(vec_top(0), vec_top(1), float(h))); + int id = (int)vertices.size() - 1; + facets.emplace_back( 0, id - 1, id - 3); // top + facets.emplace_back(id, 1, id - 2); // bottom + facets.emplace_back(id, id - 2, id - 3); // upper-right of side + facets.emplace_back(id, id - 3, id - 1); // bottom-left of side + } + // Connect the last set of vertices with the first. + int id = (int)vertices.size() - 1; + facets.emplace_back( 0, 2, id - 1); + facets.emplace_back( 3, 1, id); + facets.emplace_back(id, 2, 3); + facets.emplace_back(id, id - 1, 2); + + return mesh; +} + indexed_triangle_set its_make_cone(double r, double h, double fa) { indexed_triangle_set mesh; @@ -984,61 +1044,6 @@ indexed_triangle_set its_make_cone(double r, double h, 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 + 0.25 * M_PI; - 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_make_pyramid(float base, float height) { float a = base / 2.f; @@ -1116,6 +1121,182 @@ 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 + 0.25 * M_PI; + 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_make_snap(double r, double h, float space_proportion, float bulge_proportion) +{ + const float radius = (float)r; + const float height = (float)h; + const size_t sectors_cnt = 10; //(float)fa; + const float halfPI = 0.5f * (float)PI; + + const float space_len = space_proportion * radius; + + const float b_len = radius; + const float m_len = (1 + bulge_proportion) * radius; + const float t_len = 0.5f * radius; + + const float b_height = 0.f; + const float m_height = 0.5f * height; + const float t_height = height; + + const float b_angle = acos(space_len/b_len); + const float t_angle = acos(space_len/t_len); + + const float b_angle_step = b_angle / (float)sectors_cnt; + const float t_angle_step = t_angle / (float)sectors_cnt; + + const Vec2f b_vec = Eigen::Vector2f(0, b_len); + const Vec2f t_vec = Eigen::Vector2f(0, t_len); + + + auto add_side_vertices = [b_vec, t_vec, b_height, m_height, t_height](std::vector& vertices, float b_angle, float t_angle, const Vec2f& m_vec) { + Vec2f b_pt = Eigen::Rotation2Df(b_angle) * b_vec; + Vec2f m_pt = Eigen::Rotation2Df(b_angle) * m_vec; + Vec2f t_pt = Eigen::Rotation2Df(t_angle) * t_vec; + + vertices.emplace_back(Vec3f(b_pt(0), b_pt(1), b_height)); + vertices.emplace_back(Vec3f(m_pt(0), m_pt(1), m_height)); + vertices.emplace_back(Vec3f(t_pt(0), t_pt(1), t_height)); + }; + + auto add_side_facets = [](std::vector& facets, int vertices_cnt, int frst_id, int scnd_id) { + int id = vertices_cnt - 1; + + facets.emplace_back(frst_id, id - 2, id - 5); + + facets.emplace_back(id - 2, id - 1, id - 5); + facets.emplace_back(id - 1, id - 4, id - 5); + facets.emplace_back(id - 4, id - 1, id); + facets.emplace_back(id, id - 3, id - 4); + + facets.emplace_back(id, scnd_id, id - 3); + }; + + const float f = (b_len - m_len) / m_len; // Flattening + + auto get_m_len = [b_len, f](float angle) { + const float rad_sqr = b_len * b_len; + const float sin_sqr = sin(angle) * sin(angle); + const float f_sqr = (1-f)*(1-f); + return sqrtf(rad_sqr / (1 + (1 / f_sqr - 1) * sin_sqr)); + }; + + auto add_sub_mesh = [add_side_vertices, add_side_facets, get_m_len, + b_height, t_height, b_angle, t_angle, b_angle_step, t_angle_step] + (indexed_triangle_set& mesh, float center_x, float angle_rotation, int frst_vertex_id) { + auto& vertices = mesh.vertices; + auto& facets = mesh.indices; + + // 2 special vertices, top and bottom center, rest are relative to this + vertices.emplace_back(Vec3f(center_x, 0.f, b_height)); + vertices.emplace_back(Vec3f(center_x, 0.f, t_height)); + + float b_angle_start = angle_rotation - b_angle; + float t_angle_start = angle_rotation - t_angle; + const float b_angle_stop = angle_rotation + b_angle; + + const int frst_id = frst_vertex_id; + const int scnd_id = frst_id + 1; + + // add first side vertices and internal facets + { + const Vec2f m_vec = Eigen::Vector2f(0, get_m_len(b_angle_start)); + add_side_vertices(vertices, b_angle_start, t_angle_start, m_vec); + + int id = (int)vertices.size() - 1; + + facets.emplace_back(frst_id, id - 2, id - 1); + facets.emplace_back(frst_id, id - 1, id); + facets.emplace_back(frst_id, id, scnd_id); + } + + // add d side vertices and facets + while (!is_approx(b_angle_start, b_angle_stop)) { + b_angle_start += b_angle_step; + t_angle_start += t_angle_step; + + const Vec2f m_vec = Eigen::Vector2f(0, get_m_len(b_angle_start)); + add_side_vertices(vertices, b_angle_start, t_angle_start, m_vec); + + add_side_facets(facets, (int)vertices.size(), frst_id, scnd_id); + } + + // add last internal facets to close the mesh + { + int id = (int)vertices.size() - 1; + + facets.emplace_back(frst_id, scnd_id, id); + facets.emplace_back(frst_id, id, id - 1); + facets.emplace_back(frst_id, id - 1, id - 2); + } + }; + + + indexed_triangle_set mesh; + + mesh.vertices.reserve(2 * (3 * (2 * sectors_cnt + 1) + 2)); + mesh.indices.reserve(2 * (6 * 2 * sectors_cnt + 6)); + + add_sub_mesh(mesh, -space_len, halfPI , 0); + add_sub_mesh(mesh, space_len, 3 * halfPI, (int)mesh.vertices.size()); + + 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 9afaddd4eb..12002cc535 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -1,3 +1,14 @@ +///|/ Copyright (c) Prusa Research 2017 - 2023 Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv, Tomáš Mészáros @tamasmeszaros, Enrico Turri @enricoturri1966, Filip Sykala @Jony01 +///|/ Copyright (c) 2019 Sijmen Schoon +///|/ Copyright (c) 2016 Joseph Lenox @lordofhyphens +///|/ Copyright (c) Slic3r 2013 - 2016 Alessandro Ranellucci @alranel +///|/ +///|/ ported from lib/Slic3r/TriangleMesh.pm: +///|/ Copyright (c) Slic3r 2011 - 2014 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2012 - 2013 Mark Hindess +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef slic3r_TriangleMesh_hpp_ #define slic3r_TriangleMesh_hpp_ @@ -337,9 +348,11 @@ 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(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); +indexed_triangle_set its_make_snap(double r, double h, float space_proportion = 0.25f, float bulge_proportion = 0.125f); indexed_triangle_set its_convex_hull(const std::vector &pts); inline indexed_triangle_set its_convex_hull(const indexed_triangle_set &its) { return its_convex_hull(its.vertices); } diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index 3fd0da6604..204b267c22 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2021 - 2023 Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena, Pavel Mikuš @Godrak, Lukáš Hejl @hejllukas +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "ClipperUtils.hpp" #include "Geometry.hpp" #include "Tesselate.hpp" @@ -2300,79 +2304,4 @@ void cut_mesh(const indexed_triangle_set& mesh, float z, indexed_triangle_set* u } } -// BBS: implement plane cut with cgal -static Vec3d calc_plane_normal(const std::array& plane_points) -{ - Vec3d v01 = plane_points[1] - plane_points[0]; - Vec3d v12 = plane_points[2] - plane_points[1]; - - Vec3d plane_normal = v01.cross(v12); - plane_normal.normalize(); - return plane_normal; -} - -void cut_mesh -( - const indexed_triangle_set& mesh, // model object coordinate - std::array plane_points, // model object coordinate - indexed_triangle_set* upper, - indexed_triangle_set* lower, - bool triangulate_caps -) -{ - assert(upper || lower); - if (upper == nullptr && lower == nullptr) - return; - - BOOST_LOG_TRIVIAL(trace) << "cut_mesh - slicing object"; - - Vec3d plane_normal = calc_plane_normal(plane_points); - if (std::abs(plane_normal(0)) < EPSILON && std::abs(plane_normal(1)) < EPSILON) { - cut_mesh(mesh, plane_points[0](2), upper, lower); - return; - } - - // BBS - if (std::abs(plane_normal(2)) < EPSILON) { - // keep the side on the normal direction - } - else if (plane_normal(2) < 0.0) { - std::reverse(plane_points.begin(), plane_points.end()); - } - plane_normal = calc_plane_normal(plane_points); - - Vec3d mid_point = { 0.0, 0.0, 0.0 }; - for (auto pt : plane_points) - mid_point += pt; - mid_point /= (double)plane_points.size(); - Vec3d movement = -mid_point; - - Vec3d axis = { 0.0, 0.0, 0.0 }; - double phi = 0.0; - Matrix3d matrix; - matrix.setIdentity(); - Geometry::rotation_from_two_vectors(plane_normal, { 0.0, 0.0, 1.0 }, axis, phi, &matrix); - Vec3d angles = Geometry::extract_euler_angles(matrix); - - movement = matrix * movement; - Transform3d transfo; - transfo.setIdentity(); - transfo.translate(movement); - transfo.rotate(Eigen::AngleAxisd(angles(2), Vec3d::UnitZ()) * Eigen::AngleAxisd(angles(1), Vec3d::UnitY()) * Eigen::AngleAxisd(angles(0), Vec3d::UnitX())); - - indexed_triangle_set mesh_temp = mesh; - its_transform(mesh_temp, transfo); - cut_mesh(mesh_temp, 0., upper, lower); - - Transform3d transfo_inv = transfo.inverse(); - if (upper) { - its_transform(*upper, transfo_inv); - } - - if (lower) { - its_transform(*lower, transfo_inv); - } -} - - } // namespace Slic3r diff --git a/src/libslic3r/TriangleMeshSlicer.hpp b/src/libslic3r/TriangleMeshSlicer.hpp index 1a8b643629..a7ad62acd9 100644 --- a/src/libslic3r/TriangleMeshSlicer.hpp +++ b/src/libslic3r/TriangleMeshSlicer.hpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2021 - 2022 Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef slic3r_TriangleMeshSlicer_hpp_ #define slic3r_TriangleMeshSlicer_hpp_ @@ -130,14 +134,6 @@ void cut_mesh( indexed_triangle_set *lower, bool triangulate_caps = true); -// BBS -void cut_mesh( - const indexed_triangle_set &mesh, - std::array plane_points, - indexed_triangle_set *upper, - indexed_triangle_set *lower, - bool triangulate_caps = true); - -} +} // namespace Slic3r #endif // slic3r_TriangleMeshSlicer_hpp_ diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 08f10b2887..ce12a33486 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -160,6 +160,11 @@ void flush_logs(); // This type is only needed for Perl bindings to relay to Perl that the string is raw, not UTF-8 encoded. typedef std::string local_encoded_string; +// Returns next utf8 sequence length. =number of bytes in string, that creates together one utf-8 character. +// Starting at pos. ASCII characters returns 1. Works also if pos is in the middle of the sequence. +extern size_t get_utf8_sequence_length(const std::string& text, size_t pos = 0); +extern size_t get_utf8_sequence_length(const char *seq, size_t size); + // Convert an UTF-8 encoded string into local coding. // On Windows, the UTF-8 string is converted to a local 8-bit code page. // On OSX and Linux, this function does no conversion and returns a copy of the source string. diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index f5915175c7..e31656cda9 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -1,3 +1,9 @@ +///|/ Copyright (c) Prusa Research 2016 - 2023 Pavel Mikuš @Godrak, Oleksandra Iushchenko @YuSanka, Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena, Filip Sykala @Jony01, David Kocík @kocikdav, Roman Beránek @zavorka, Enrico Turri @enricoturri1966, Tomáš Mészáros @tamasmeszaros, Vojtěch Král @vojtechkral +///|/ Copyright (c) 2021 Justin Schuh @jschuh +///|/ Copyright (c) Slic3r 2013 - 2015 Alessandro Ranellucci @alranel +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "Utils.hpp" #include "I18N.hpp" @@ -1030,6 +1036,76 @@ bool is_shapes_dir(const std::string& dir) namespace Slic3r { +size_t get_utf8_sequence_length(const std::string& text, size_t pos) +{ + assert(pos < text.size()); + return get_utf8_sequence_length(text.c_str() + pos, text.size() - pos); +} + +size_t get_utf8_sequence_length(const char *seq, size_t size) +{ + size_t length = 0; + unsigned char c = seq[0]; + if (c < 0x80) { // 0x00-0x7F + // is ASCII letter + length++; + } + // Bytes 0x80 to 0xBD are trailer bytes in a multibyte sequence. + // pos is in the middle of a utf-8 sequence. Add the utf-8 trailer bytes. + else if (c < 0xC0) { // 0x80-0xBF + length++; + while (length < size) { + c = seq[length]; + if (c < 0x80 || c >= 0xC0) { + break; // prevent overrun + } + length++; // add a utf-8 trailer byte + } + } + // Bytes 0xC0 to 0xFD are header bytes in a multibyte sequence. + // The number of one bits above the topmost zero bit indicates the number of bytes (including this one) in the whole sequence. + else if (c < 0xE0) { // 0xC0-0xDF + // add a utf-8 sequence (2 bytes) + if (2 > size) { + return size; // prevent overrun + } + length += 2; + } + else if (c < 0xF0) { // 0xE0-0xEF + // add a utf-8 sequence (3 bytes) + if (3 > size) { + return size; // prevent overrun + } + length += 3; + } + else if (c < 0xF8) { // 0xF0-0xF7 + // add a utf-8 sequence (4 bytes) + if (4 > size) { + return size; // prevent overrun + } + length += 4; + } + else if (c < 0xFC) { // 0xF8-0xFB + // add a utf-8 sequence (5 bytes) + if (5 > size) { + return size; // prevent overrun + } + length += 5; + } + else if (c < 0xFE) { // 0xFC-0xFD + // add a utf-8 sequence (6 bytes) + if (6 > size) { + return size; // prevent overrun + } + length += 6; + } + else { // 0xFE-0xFF + // not a utf-8 sequence + length++; + } + return length; +} + // Encode an UTF-8 string to the local code page. std::string encode_path(const char *src) { diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index e1404b09a5..173b0ce012 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -132,8 +132,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoFdmSupports.hpp GUI/Gizmos/GLGizmoFlatten.cpp GUI/Gizmos/GLGizmoFlatten.hpp - GUI/Gizmos/GLGizmoAdvancedCut.cpp - GUI/Gizmos/GLGizmoAdvancedCut.hpp + GUI/Gizmos/GLGizmoCut.cpp + GUI/Gizmos/GLGizmoCut.hpp #GUI/Gizmos/GLGizmoHollow.cpp #GUI/Gizmos/GLGizmoHollow.hpp GUI/Gizmos/GLGizmoPainterBase.cpp diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index 0d52ba4882..ce10a463f2 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -411,6 +411,12 @@ private: // plane coeffs for clipping in shaders std::array m_clipping_plane; + // plane coeffs for render volumes with different colors in shaders + // used by cut gizmo + std::array m_color_clip_plane; + bool m_use_color_clip_plane{ false }; + std::array m_color_clip_plane_colors{ ColorRGBA::RED(), ColorRGBA::BLUE() }; + struct Slope { // toggle for slope rendering @@ -482,6 +488,14 @@ public: const std::array& get_z_range() const { return m_z_range; } const std::array& get_clipping_plane() const { return m_clipping_plane; } + void set_use_color_clip_plane(bool use) { m_use_color_clip_plane = use; } + void set_color_clip_plane(const Vec3d& cp_normal, double offset) { + for (int i = 0; i < 3; ++i) + m_color_clip_plane[i] = -cp_normal[i]; + m_color_clip_plane[3] = offset; + } + void set_color_clip_plane_colors(const std::array& colors) { m_color_clip_plane_colors = colors; } + bool is_slope_GlobalActive() const { return m_slope.isGlobalActive; } bool is_slope_active() const { return m_slope.active; } void set_slope_active(bool active) { m_slope.active = active; } diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index afcd5bdcb8..523b0d5f6e 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -807,6 +807,10 @@ public: bool get_use_clipping_planes() const { return m_use_clipping_planes; } const std::array &get_clipping_planes() const { return m_clipping_planes; }; + void set_use_color_clip_plane(bool use) { m_volumes.set_use_color_clip_plane(use); } + void set_color_clip_plane(const Vec3d& cp_normal, double offset) { m_volumes.set_color_clip_plane(cp_normal, offset); } + void set_color_clip_plane_colors(const std::array& colors) { m_volumes.set_color_clip_plane_colors(colors); } + void refresh_camera_scene_box(); BoundingBoxf3 volumes_bounding_box(bool current_plate_only = false) const; diff --git a/src/slic3r/GUI/GLSelectionRectangle.cpp b/src/slic3r/GUI/GLSelectionRectangle.cpp index ecced3c564..68f9d096eb 100644 --- a/src/slic3r/GUI/GLSelectionRectangle.cpp +++ b/src/slic3r/GUI/GLSelectionRectangle.cpp @@ -1,5 +1,10 @@ +///|/ Copyright (c) Prusa Research 2019 - 2022 Enrico Turri @enricoturri1966, Filip Sykala @Jony01, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "GLSelectionRectangle.hpp" #include "Camera.hpp" +#include "CameraUtils.hpp" #include "3DScene.hpp" #include "GLCanvas3D.hpp" #include "GUI_App.hpp" @@ -29,35 +34,19 @@ namespace GUI { m_end_corner = mouse_position; } - std::vector GLSelectionRectangle::stop_dragging(const GLCanvas3D& canvas, const std::vector& points) + std::vector GLSelectionRectangle::contains(const std::vector& points) const { std::vector out; - if (!is_dragging()) - return out; - - m_state = Off; - - const Camera& camera = wxGetApp().plater()->get_camera(); - Matrix4d modelview = camera.get_view_matrix().matrix(); - Matrix4d projection= camera.get_projection_matrix().matrix(); - Vec4i viewport(camera.get_viewport().data()); - - // Convert our std::vector to Eigen dynamic matrix. - Eigen::Matrix pts(points.size(), 3); - for (size_t i=0; i(i, 0) = points[i]; - - // Get the projections. - Eigen::Matrix projections; - igl::project(pts, modelview, projection, viewport, projections); - // bounding box created from the rectangle corners - will take care of order of the corners - BoundingBox rectangle(Points{ Point(m_start_corner.cast()), Point(m_end_corner.cast()) }); + const BoundingBox rectangle(Points{ Point(m_start_corner.cast()), Point(m_end_corner.cast()) }); // Iterate over all points and determine whether they're in the rectangle. - for (int i = 0; iget_camera(); + Points points_2d = CameraUtils::project(camera, points); + unsigned int size = static_cast(points.size()); + for (unsigned int i = 0; i< size; ++i) + if (rectangle.contains(points_2d[i])) out.push_back(i); return out; diff --git a/src/slic3r/GUI/GLSelectionRectangle.hpp b/src/slic3r/GUI/GLSelectionRectangle.hpp index 5ec56a60a0..8d87e18e46 100644 --- a/src/slic3r/GUI/GLSelectionRectangle.hpp +++ b/src/slic3r/GUI/GLSelectionRectangle.hpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2019 - 2022 Enrico Turri @enricoturri1966, Lukáš Matěna @lukasmatena +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef slic3r_GLSelectionRectangle_hpp_ #define slic3r_GLSelectionRectangle_hpp_ @@ -25,8 +29,8 @@ public: void dragging(const Vec2d& mouse_position); // Given a vector of points in world coordinates, the function returns indices of those - // that are in the rectangle. It then disables the rectangle. - std::vector stop_dragging(const GLCanvas3D& canvas, const std::vector& points); + // that are in the rectangle. + std::vector contains(const std::vector& points) const; // Disables the rectangle. void stop_dragging(); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index ad885ebb4e..4d632d17eb 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2811,7 +2811,7 @@ void ObjectList::merge(bool to_multipart_object) } } -void ObjectList::merge_volumes() +/*void ObjectList::merge_volumes() { std::vector obj_idxs, vol_idxs; get_selection_indexes(obj_idxs, vol_idxs); @@ -2839,11 +2839,11 @@ void ObjectList::merge_volumes() else { for (int vol_idx : vol_idxs) selection.add_volume(last_obj_idx, vol_idx, 0, false); - }*/ + }#1# #else wxGetApp().plater()->merge(obj_idxs[0], vol_idxs); #endif -} +}*/ void ObjectList::layers_editing() { diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index f4a6f0320a..8bd1e8fbdf 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -297,7 +297,7 @@ public: void del_info_item(const int obj_idx, InfoItemType type); void split(); void merge(bool to_multipart_object); - void merge_volumes(); // BBS: merge parts to single part + // void merge_volumes(); // BBS: merge parts to single part void layers_editing(); void boolean(); // BBS: Boolean Operation of parts diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp index bcd4f2096f..d60e504856 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp @@ -222,6 +222,9 @@ public: 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(); } + virtual bool is_in_editing_mode() const { return false; } + virtual bool is_selection_rectangle_dragging() const { return false; } + protected: float last_input_window_width = 0; virtual bool on_init() = 0; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 0e5d1c7308..7f6e9fc1a8 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -1,344 +1,3776 @@ -// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. +///|/ Copyright (c) Prusa Research 2019 - 2023 Oleksandra Iushchenko @YuSanka, Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena, Enrico Turri @enricoturri1966, Filip Sykala @Jony01, Vojtěch Král @vojtechkral +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "GLGizmoCut.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #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/Gizmos/GizmoObjectManipulation.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" +#include "slic3r/GUI/Field.hpp" +#include "slic3r/GUI/MsgDialog.hpp" + 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 PLANE_COLOR = { 0.8f, 0.8f, 0.8f, 0.5f }; +static const ColorRGBA GRABBER_COLOR = ColorRGBA::YELLOW(); +static const ColorRGBA UPPER_PART_COLOR = ColorRGBA::CYAN(); +static const ColorRGBA LOWER_PART_COLOR = ColorRGBA::MAGENTA(); +static const ColorRGBA MODIFIER_COLOR = ColorRGBA(0.75f, 0.75f, 0.75f, 0.5f); -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); +static const ColorRGBA HOVERED_ERR_COLOR = ColorRGBA(1.0f, 0.3f, 0.3f, 1.0f); + +static const ColorRGBA CUT_PLANE_DEF_COLOR = ColorRGBA(0.9f, 0.9f, 0.9f, 0.5f); +static const ColorRGBA CUT_PLANE_ERR_COLOR = ColorRGBA(1.0f, 0.8f, 0.8f, 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; +} + +//! -- #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 (GrabberID::Count) + , m_connector_type (CutConnectorType::Plug) + , m_connector_style (int(CutConnectorStyle::Prism)) + , m_connector_shape_id (int(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("Dovetail")//, _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") }; + + std::map connetor_types = { + {ImGui::PlugMarker , _u8L("Plug") }, + {ImGui::DowelMarker, _u8L("Dowel") }, + //TRN Connectors type next to "Plug" and "Dowel" + {ImGui::SnapMarker, _u8L("Snap") }, + }; + for (auto connector : connetor_types) { + std::string type_label = " " + connector.second + " "; + type_label += connector.first; + m_connector_types.push_back(type_label); + } + + m_connector_styles = { _u8L("Prism"), _u8L("Frustum") +// , _u8L("Claw") + }; + + m_connector_shapes = { _u8L("Triangle"), _u8L("Square"), _u8L("Hexagon"), _u8L("Circle") +// , _u8L("D-shape") + }; + + m_axis_names = { "X", "Y", "Z" }; + + m_part_orientation_names = { + {"none", _L("Keep orientation")}, + {"on_cut", _L("Place on cut")}, + {"flip", _L("Flip upside down")}, + }; + + m_labels_map = { + {"Connectors" , _u8L("Connectors")}, + {"Type" , _u8L("Type")}, + {"Style" , _u8L("Style")}, + {"Shape" , _u8L("Shape")}, + {"Depth" , _u8L("Depth")}, + {"Size" , _u8L("Size")}, + {"Rotation" , _u8L("Rotation")}, + {"Groove" , _u8L("Groove")}, + {"Width" , _u8L("Width")}, + {"Flap Angle" , _u8L("Flap Angle")}, + {"Groove Angle" , _u8L("Groove Angle")}, + }; + +// update_connector_shape(); } -bool GLGizmoCut::on_init() +std::string GLGizmoCut3D::get_tooltip() const { - m_grabbers.emplace_back(); - m_shortcut_key = WXK_CONTROL_C; - return true; + std::string tooltip; + if (m_hover_id == Z || (m_dragging && m_hover_id == CutPlane)) { + double koef = m_imperial_units ? GizmoObjectManipulation::mm_to_in : 1.0; + std::string unit_str = " " + (m_imperial_units ? _u8L("in") : _u8L("mm")); + const BoundingBoxf3& tbb = m_transformed_bounding_box; + + const std::string name = m_keep_as_parts ? _u8L("Part") : _u8L("Object"); + if (tbb.max.z() >= 0.0) { + double top = (tbb.min.z() <= 0.0 ? tbb.max.z() : tbb.size().z()) * koef; + tooltip += format(static_cast(top), 2) + " " + unit_str + " (" + name + " A)"; + 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(static_cast(bottom), 2) + " " + unit_str + " (" + name + " B)"; + } + return tooltip; + } + + if (!m_dragging && m_hover_id == CutPlane) { + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) + return _u8L("Click to flip the cut plane\n" + "Drag to move the cut plane"); + return _u8L("Click to flip the cut plane\n" + "Drag to move the cut plane\n" + "Right-click a part to assign it to the other side"); + } + + if (tooltip.empty() && (m_hover_id == X || m_hover_id == Y || m_hover_id == CutPlaneZRotation)) { + std::string axis = m_hover_id == X ? "X" : m_hover_id == Y ? "Y" : "Z"; + return axis + ": " + format(float(rad2deg(m_angle)), 1) + _u8L("°"); + } + + return tooltip; } -std::string GLGizmoCut::on_get_name() const +bool GLGizmoCut3D::on_mouse(const wxMouseEvent &mouse_event) { - return _u8L("Cut"); + 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 (mouse_event.CmdDown() && mouse_event.LeftDown()) + return false; + 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 (m_hover_id >= CutPlane && mouse_event.LeftDown() && !m_connectors_editing) { + // before processing of a use_grabbers(), detect start move position as a projection of mouse position to the cut plane + Vec3d pos; + Vec3d pos_world; + if (unproject_on_cut_plane(mouse_pos, pos, pos_world, false)) + m_cut_plane_start_move_pos = pos_world; + } + + 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()); + } + else if (m_hover_id == CutPlane) { + if (mouse_event.LeftDown()) { + m_was_cut_plane_dragged = m_was_contour_selected = false; + + // disable / enable current contour + Vec3d pos; + Vec3d pos_world; + m_was_contour_selected = unproject_on_cut_plane(mouse_pos.cast(), pos, pos_world); + if (m_was_contour_selected) { + // Following would inform the clipper about the mouse click, so it can + // toggle the respective contour as disabled. + //m_c->object_clipper()->pass_mouse_click(pos_world); + //process_contours(); + return true; + } + + } + else if (mouse_event.LeftUp() && !m_was_cut_plane_dragged && !m_was_contour_selected) + flip_cut_plane(); + } + + if (m_hover_id >= CutPlane && mouse_event.Dragging() && !m_connectors_editing) { + // if we continue to dragging a cut plane, than update a start move position as a projection of mouse position to the cut plane after processing of a use_grabbers() + Vec3d pos; + Vec3d pos_world; + if (unproject_on_cut_plane(mouse_pos, pos, pos_world, false)) + m_cut_plane_start_move_pos = pos_world; + } + + toggle_model_objects_visibility(); + 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_connectors_editing && mouse_event.GetModifiers() == wxMOD_NONE && + CutMode(m_mode) == CutMode::cutPlanar) { + // Check the internal part raycasters. + if (! m_part_selection.valid()) + process_contours(); + m_part_selection.toggle_selection(mouse_pos); + check_and_update_connectors_state(); // after a contour is deactivated, its connectors are inside the object + return true; + } + + 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; } -void GLGizmoCut::on_set_state() +void GLGizmoCut3D::shift_cut(double delta) { - // Reset m_cut_z on gizmo activation - if (get_state() == On) - m_cut_z = bounding_box().center().z(); + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Move cut plane"), UndoRedo::SnapshotType::GizmoAction); + set_center(m_plane_center + m_cut_normal * delta, true); + m_ar_plane_center = m_plane_center; } -bool GLGizmoCut::on_is_activable() const +void GLGizmoCut3D::rotate_vec3d_around_plane_center(Vec3d&vec) { - const Selection& selection = m_parent.get_selection(); - return selection.is_single_full_instance() && !selection.is_wipe_tower(); + vec = Transformation(translation_transform(m_plane_center) * m_rotation_m * translation_transform(-m_plane_center)).get_matrix() * vec; } -void GLGizmoCut::on_start_dragging() +void GLGizmoCut3D::put_connectors_on_cut_plane(const Vec3d& cp_normal, double cp_offset) { - if (m_hover_id == -1) + 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; + } + } +} + +// returns true if the camera (forward) is pointing in the negative direction of the cut normal +bool GLGizmoCut3D::is_looking_forward() const +{ + const Camera& camera = wxGetApp().plater()->get_camera(); + const double dot = camera.get_dir_forward().dot(m_cut_normal); + return dot < 0.05; +} + +void GLGizmoCut3D::update_clipper() +{ + // update cut_normal + Vec3d normal = m_rotation_m * Vec3d::UnitZ(); + normal.normalize(); + m_cut_normal = normal; + + // calculate normal and offset for clipping plane + Vec3d beg = m_bb_center; + beg[Z] -= m_radius; + rotate_vec3d_around_plane_center(beg); + + m_clp_normal = normal; + double offset = normal.dot(m_plane_center); + double dist = normal.dot(beg); + + m_parent.set_color_clip_plane(normal, offset); + + if (!is_looking_forward()) { + // recalculate normal and offset for clipping plane, if camera is looking downward to cut plane + normal = m_rotation_m * (-1. * Vec3d::UnitZ()); + normal.normalize(); + + beg = m_bb_center; + beg[Z] += m_radius; + rotate_vec3d_around_plane_center(beg); + + m_clp_normal = normal; + offset = normal.dot(m_plane_center); + dist = normal.dot(beg); + } + + 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::set_center(const Vec3d& center, bool update_tbb /*=false*/) +{ + set_center_pos(center, update_tbb); + check_and_update_connectors_state(); + update_clipper(); +} + +void GLGizmoCut3D::switch_to_mode(size_t new_mode) +{ + m_mode = new_mode; + update_raycasters_for_picking(); + + apply_color_clip_plane_colors(); + if (auto oc = m_c->object_clipper()) { + m_contour_width = CutMode(m_mode) == CutMode::cutTongueAndGroove ? 0.f : 0.4f; + oc->set_behavior(m_connectors_editing, m_connectors_editing, double(m_contour_width)); + } + + update_plane_model(); + reset_cut_by_contours(); +} + +bool GLGizmoCut3D::render_cut_mode_combo() +{ + ImGui::AlignTextToFramePadding(); + int selection_idx = int(m_mode); + const bool is_changed = m_imgui->combo(_u8L("Mode"), m_modes, selection_idx, 0, m_label_width, m_control_width); + + if (is_changed) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Change cut mode"), UndoRedo::SnapshotType::GizmoAction); + switch_to_mode(size_t(selection_idx)); + check_and_update_connectors_state(); + } + + return is_changed; +} + +bool GLGizmoCut3D::render_combo(const std::string& label, const std::vector& lines, int& selection_idx) +{ + ImGui::AlignTextToFramePadding(); + const bool is_changed = m_imgui->combo(label, lines, selection_idx, 0, m_label_width, m_control_width); + + //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 *= GizmoObjectManipulation::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 ? GizmoObjectManipulation::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, float min_val/* = -0.1f*/, float max_tolerance/* = -0.1f*/) +{ + static constexpr const float UndefMinVal = -0.1f; + const float f_mm_to_in = static_cast(GizmoObjectManipulation::mm_to_in); + + auto render_slider = [this, f_mm_to_in] + (const std::string& label, float& val, float def_val, float max_val, const wxString& tooltip) { + float min_val = val < 0.f ? UndefMinVal : def_val; + float value = val; + if (m_imperial_units) { + min_val *= f_mm_to_in; + value *= f_mm_to_in; + } + const float old_val = value; + const std::string format = val < 0.f ? UndefLabel : (m_imperial_units ? "%.4f " + _u8L("in") : "%.2f " + _u8L("mm")); + + m_imgui->slider_float(label.c_str(), &value, min_val, max_val, format.c_str(), 1.f, true, tooltip); + val = value * (m_imperial_units ? static_cast(GizmoObjectManipulation::in_to_mm) : 1.f); + + m_is_slider_editing_done |= m_imgui->get_last_slider_status().deactivated_after_edit; + + return !is_approx(old_val, value); + }; + + const BoundingBoxf3 bbox = m_bounding_box; + const float mean_size = float((bbox.size().x() + bbox.size().y() + bbox.size().z()) / 9.0) * (m_imperial_units ? f_mm_to_in : 1.f); + const float min_v = min_val > 0.f ? /*std::min(max_val, mean_size)*/min_val : 1.f; + + m_imgui->text(label); + + ImGui::SameLine(m_label_width); + ImGui::PushItemWidth(m_control_width * 0.7f); + +// const bool is_value_changed = render_slider("##" + label, value_in, 1.f, mean_size, _L("Value")); + const bool is_value_changed = render_slider("##" + label, value_in, min_v, mean_size, _L("Value")); + + ImGui::SameLine(); + ImGui::PushItemWidth(m_control_width * 0.45f); + +// const bool is_tolerance_changed = render_slider("##tolerance_" + label, tolerance_in, 0.f, 0.5f * mean_size, _L("Tolerance")); + const float max_tolerance_v = max_tolerance > 0.f ? std::min(max_tolerance, 0.5f * mean_size) : 0.5f * mean_size; + const bool is_tolerance_changed = render_slider("##tolerance_" + label, tolerance_in, 0.f, max_tolerance_v, _L("Tolerance")); + + return is_value_changed || is_tolerance_changed; +} + +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 *= GizmoObjectManipulation::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 ? GizmoObjectManipulation::in_to_mm : 1.0); + + if (in_val != val) { + move[axis] = val; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Move cut plane"), UndoRedo::SnapshotType::GizmoAction); + set_center(move, true); + m_ar_plane_center = m_plane_center; + + reset_cut_by_contours(); + } +} + +bool GLGizmoCut3D::render_connect_type_radio_button(CutConnectorType type) +{ + ImGui::SameLine(type == CutConnectorType::Plug ? m_label_width : 0); + ImGui::PushItemWidth(m_control_width); + if (ImGui::RadioButton(m_connector_types[size_t(type)].c_str(), 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 (ImGui::RadioButton(m_connector_modes[int(mode)].c_str(), 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::RevertBtn; + 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 double get_grabber_mean_size(const BoundingBoxf3& bb) +{ + return (bb.size().x() + bb.size().y() + bb.size().z()) / 30.; +} + +indexed_triangle_set GLGizmoCut3D::its_make_groove_plane() +{ + // values for calculation + + const float side_width = is_approx(m_groove.flaps_angle, 0.f) ? m_groove.depth : (m_groove.depth / sin(m_groove.flaps_angle)); + const float flaps_width = 2.f * side_width * cos(m_groove.flaps_angle); + + const float groove_half_width_upper = 0.5f * (m_groove.width); + const float groove_half_width_lower = 0.5f * (m_groove.width + flaps_width); + + const float cut_plane_radius = 1.5f * float(m_radius); + const float cut_plane_length = 1.5f * cut_plane_radius; + + const float groove_half_depth = 0.5f * m_groove.depth; + + const float x = 0.5f * cut_plane_radius; + const float y = 0.5f * cut_plane_length; + float z_upper = groove_half_depth; + float z_lower = -groove_half_depth; + + const float proj = y * tan(m_groove.angle); + + float ext_upper_x = groove_half_width_upper + proj; // upper_x extension + float ext_lower_x = groove_half_width_lower + proj; // lower_x extension + + float nar_upper_x = groove_half_width_upper - proj; // upper_x narrowing + float nar_lower_x = groove_half_width_lower - proj; // lower_x narrowing + + const float cut_plane_thiknes = 0.02f;// 0.02f * (float)get_grabber_mean_size(m_bounding_box); // cut_plane_thiknes + + // Vertices of the groove used to detection if groove is valid + // They are written as: + // {left_ext_lower, left_nar_lower, left_ext_upper, left_nar_upper, + // right_ext_lower, right_nar_lower, right_ext_upper, right_nar_upper } + { + m_groove_vertices.clear(); + m_groove_vertices.reserve(8); + + m_groove_vertices.emplace_back(Vec3f(-ext_lower_x, -y, z_lower).cast()); + m_groove_vertices.emplace_back(Vec3f(-nar_lower_x, y, z_lower).cast()); + m_groove_vertices.emplace_back(Vec3f(-ext_upper_x, -y, z_upper).cast()); + m_groove_vertices.emplace_back(Vec3f(-nar_upper_x, y, z_upper).cast()); + m_groove_vertices.emplace_back(Vec3f( ext_lower_x, -y, z_lower).cast()); + m_groove_vertices.emplace_back(Vec3f( nar_lower_x, y, z_lower).cast()); + m_groove_vertices.emplace_back(Vec3f( ext_upper_x, -y, z_upper).cast()); + m_groove_vertices.emplace_back(Vec3f( nar_upper_x, y, z_upper).cast()); + } + + // Different cases of groove plane: + + // groove is open + + if (groove_half_width_upper > proj && groove_half_width_lower > proj) { + indexed_triangle_set mesh; + + auto get_vertices = [x, y](float z_upper, float z_lower, float nar_upper_x, float nar_lower_x, float ext_upper_x, float ext_lower_x) { + return std::vector({ + // upper left part vertices + {-x, -y, z_upper}, {-x, y, z_upper}, {-nar_upper_x, y, z_upper}, {-ext_upper_x, -y, z_upper}, + // lower part vertices + {-ext_lower_x, -y, z_lower}, {-nar_lower_x, y, z_lower}, {nar_lower_x, y, z_lower}, {ext_lower_x, -y, z_lower}, + // upper right part vertices + {ext_upper_x, -y, z_upper}, {nar_upper_x, y, z_upper}, {x, y, z_upper}, {x, -y, z_upper} + }); + }; + + mesh.vertices = get_vertices(z_upper, z_lower, nar_upper_x, nar_lower_x, ext_upper_x, ext_lower_x); + mesh.vertices.reserve(2 * mesh.vertices.size()); + + z_upper -= cut_plane_thiknes; + z_lower -= cut_plane_thiknes; + + const float under_x_shift = cut_plane_thiknes / tan(0.5f * m_groove.flaps_angle); + + nar_upper_x += under_x_shift; + nar_lower_x += under_x_shift; + ext_upper_x += under_x_shift; + ext_lower_x += under_x_shift; + + std::vector vertices = get_vertices(z_upper, z_lower, nar_upper_x, nar_lower_x, ext_upper_x, ext_lower_x); + mesh.vertices.insert(mesh.vertices.end(), vertices.begin(), vertices.end()); + + mesh.indices = { + // above view + {5,4,7}, {5,7,6}, // lower part + {3,4,5}, {3,5,2}, // left side + {9,6,8}, {8,6,7}, // right side + {1,0,2}, {2,0,3}, // upper left part + {9,8,10}, {10,8,11}, // upper right part + // under view + {20,21,22}, {20,22,23}, // upper right part + {12,13,14}, {12,14,15}, // upper left part + {18,21,20}, {18,20,19}, // right side + {16,15,14}, {16,14,17}, // left side + {16,17,18}, {16,18,19}, // lower part + // left edge + {1,13,0}, {0,13,12}, + // front edge + {0,12,3}, {3,12,15}, {3,15,4}, {4,15,16}, {4,16,7}, {7,16,19}, {7,19,20}, {7,20,8}, {8,20,11}, {11,20,23}, + // right edge + {11,23,10}, {10,23,22}, + // back edge + {1,13,2}, {2,13,14}, {2,14,17}, {2,17,5}, {5,17,6}, {6,17,18}, {6,18,9}, {9,18,21}, {9,21,10}, {10,21,22} + }; + return mesh; + } + + float cross_pt_upper_y = groove_half_width_upper / tan(m_groove.angle); + + // groove is closed + + if (groove_half_width_upper < proj && groove_half_width_lower < proj) { + float cross_pt_lower_y = groove_half_width_lower / tan(m_groove.angle); + + indexed_triangle_set mesh; + + auto get_vertices = [x, y](float z_upper, float z_lower, float cross_pt_upper_y, float cross_pt_lower_y, float ext_upper_x, float ext_lower_x) { + return std::vector({ + // upper part vertices + {-x, -y, z_upper}, {-x, y, z_upper}, {x, y, z_upper}, {x, -y, z_upper}, + {ext_upper_x, -y, z_upper}, {0.f, cross_pt_upper_y, z_upper}, {-ext_upper_x, -y, z_upper}, + // lower part vertices + {-ext_lower_x, -y, z_lower}, {0.f, cross_pt_lower_y, z_lower}, {ext_lower_x, -y, z_lower} + }); + }; + + mesh.vertices = get_vertices(z_upper, z_lower, cross_pt_upper_y, cross_pt_lower_y, ext_upper_x, ext_lower_x); + mesh.vertices.reserve(2 * mesh.vertices.size()); + + z_upper -= cut_plane_thiknes; + z_lower -= cut_plane_thiknes; + + const float under_x_shift = cut_plane_thiknes / tan(0.5f * m_groove.flaps_angle); + + cross_pt_upper_y += cut_plane_thiknes; + cross_pt_lower_y += cut_plane_thiknes; + ext_upper_x += under_x_shift; + ext_lower_x += under_x_shift; + + std::vector vertices = get_vertices(z_upper, z_lower, cross_pt_upper_y, cross_pt_lower_y, ext_upper_x, ext_lower_x); + mesh.vertices.insert(mesh.vertices.end(), vertices.begin(), vertices.end()); + + mesh.indices = { + // above view + {8,7,9}, // lower part + {5,8,6}, {6,8,7}, // left side + {4,9,8}, {4,8,5}, // right side + {1,0,6}, {1,6,5},{1,5,2}, {2,5,4}, {2,4,3}, // upper part + // under view + {10,11,16}, {16,11,15}, {15,11,12}, {15,12,14}, {14,12,13}, // upper part + {18,15,14}, {14,18,19}, // right side + {17,16,15}, {17,15,18}, // left side + {17,18,19}, // lower part + // left edge + {1,11,0}, {0,11,10}, + // front edge + {0,10,6}, {6,10,16}, {6,17,16}, {6,7,17}, {7,17,19}, {7,19,9}, {4,14,19}, {4,19,9}, {4,14,13}, {4,13,3}, + // right edge + {3,13,12}, {3,12,2}, + // back edge + {2,12,11}, {2,11,1} + }; + + return mesh; + } + + // groove is closed from the roof + + indexed_triangle_set mesh; + mesh.vertices = { + // upper part vertices + {-x, -y, z_upper}, {-x, y, z_upper}, {x, y, z_upper}, {x, -y, z_upper}, + {ext_upper_x, -y, z_upper}, {0.f, cross_pt_upper_y, z_upper}, {-ext_upper_x, -y, z_upper}, + // lower part vertices + {-ext_lower_x, -y, z_lower}, {-nar_lower_x, y, z_lower}, {nar_lower_x, y, z_lower}, {ext_lower_x, -y, z_lower} + }; + + mesh.vertices.reserve(2 * mesh.vertices.size() + 1); + + z_upper -= cut_plane_thiknes; + z_lower -= cut_plane_thiknes; + + const float under_x_shift = cut_plane_thiknes / tan(0.5f * m_groove.flaps_angle); + + nar_lower_x += under_x_shift; + ext_upper_x += under_x_shift; + ext_lower_x += under_x_shift; + + std::vector vertices = { + // upper part vertices + {-x, -y, z_upper}, {-x, y, z_upper}, {x, y, z_upper}, {x, -y, z_upper}, + {ext_upper_x, -y, z_upper}, {under_x_shift, cross_pt_upper_y, z_upper}, {-under_x_shift, cross_pt_upper_y, z_upper}, {-ext_upper_x, -y, z_upper}, + // lower part vertices + {-ext_lower_x, -y, z_lower}, {-nar_lower_x, y, z_lower}, {nar_lower_x, y, z_lower}, {ext_lower_x, -y, z_lower} + }; + mesh.vertices.insert(mesh.vertices.end(), vertices.begin(), vertices.end()); + + mesh.indices = { + // above view + {8,7,10}, {8,10,9}, // lower part + {5,8,7}, {5,7,6}, // left side + {4,10,9}, {4,9,5}, // right side + {1,0,6}, {1,6,5},{1,5,2}, {2,5,4}, {2,4,3}, // upper part + // under view + {11,12,18}, {18,12,17}, {17,12,16}, {16,12,13}, {16,13,15}, {15,13,14}, // upper part + {21,16,15}, {21,15,22}, // right side + {19,18,17}, {19,17,20}, // left side + {19,20,21}, {19,21,22}, // lower part + // left edge + {1,12,11}, {1,11,0}, + // front edge + {0,11,18}, {0,18,6}, {7,19,18}, {7,18,6}, {7,19,22}, {7,22,10}, {10,22,15}, {10,15,4}, {4,15,14}, {4,14,3}, + // right edge + {3,14,13}, {3,14,2}, + // back edge + {2,13,12}, {2,12,1}, {5,16,21}, {5,21,9}, {9,21,20}, {9,20,8}, {5,17,20}, {5,20,8} + }; + + return mesh; +} + +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_update(const UpdateData& data) -{ - if (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)); - // Draw the cutting plane - ::glBegin(GL_QUADS); - ::glColor4fv(PLANE_COLOR.data()); - ::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()); + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); + + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + ColorRGBA cp_clr = can_perform_cut() && has_valid_groove() ? CUT_PLANE_DEF_COLOR : CUT_PLANE_ERR_COLOR; + if (m_mode == size_t(CutMode::cutTongueAndGroove)) + cp_clr.a(cp_clr.a() - 0.1f); + m_plane.model.set_color(cp_clr); + + const Transform3d view_model_matrix = camera.get_view_matrix() * translation_transform(m_plane_center) * m_rotation_m; + shader->set_uniform("view_model_matrix", view_model_matrix); + m_plane.model.render(); glsafe(::glEnable(GL_CULL_FACE)); glsafe(::glDisable(GL_BLEND)); - // TODO: draw cut part contour? + shader->stop_using(); +} - // Draw the grabber and the connecting line - m_grabbers[0].center = plane_center; - m_grabbers[0].center.z() = plane_center.z() + Offset; +static double get_half_size(double size) +{ + return std::max(size * 0.35, 0.05); +} +static double get_dragging_half_size(double size) +{ + return get_half_size(size) * 1.25; +} + +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("emission_factor", 0.2f); + 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 = 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(GrabberID axis, const ColorRGBA& color) +{ + GLShaderProgram* line_shader = 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 if (axis == Y) + view_model_matrix = view_model_matrix * rotation_transform(-0.5 * PI * Vec3d::UnitZ()) * rotation_transform(-0.5 * PI * Vec3d::UnitY()); + else + view_model_matrix = view_model_matrix * rotation_transform(-0.5 * PI * Vec3d::UnitZ()); + + 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, double line_len_koef/* = 1.0*/) +{ + const Transform3d line_view_matrix = view_matrix * scale_transform(Vec3d(1.0, 1.0, line_len_koef * 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)); - glsafe(::glLineWidth(m_hover_id != -1 ? 2.0f : 1.5f)); - glsafe(::glColor3f(1.0, 1.0, 0.0)); - ::glBegin(GL_LINES); - ::glVertex3dv(plane_center.data()); - ::glVertex3dv(m_grabbers[0].center.data()); - glsafe(::glEnd()); + ColorRGBA color = ColorRGBA::GRAY(); - GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); - if (shader == nullptr) - return; - shader->start_using(); - shader->set_uniform("emission_factor", 0.1f); + const Transform3d view_matrix = wxGetApp().plater()->get_camera().get_view_matrix() * translation_transform(m_plane_center) * m_rotation_m; - 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)); + const double mean_size = get_grabber_mean_size(m_bounding_box); + double size; - shader->stop_using(); + const bool no_xy_dragging = m_dragging && m_hover_id == CutPlane; - glsafe(::glPushMatrix()); - glsafe(::glTranslated(m_cut_contours.shift.x(), m_cut_contours.shift.y(), m_cut_contours.shift.z())); - glsafe(::glLineWidth(2.0f)); - m_cut_contours.contours.render(); - glsafe(::glPopMatrix()); -} + if (!no_xy_dragging && m_hover_id != CutPlaneZRotation && m_hover_id != CutPlaneXMove && m_hover_id != CutPlaneYMove) { + render_grabber_connection(GRABBER_COLOR, view_matrix); -void GLGizmoCut::on_render_for_picking() -{ - glsafe(::glDisable(GL_DEPTH_TEST)); - render_grabbers_for_picking(m_parent.get_selection().get_bounding_box()); -} - -void GLGizmoCut::on_render_input_window(float x, float y, float bottom_limit) -{ - //static float last_y = 0.0f; - //static float last_h = 0.0f; - - //BBS: GUI refactor: move gizmo to the right -#if BBS_TOOLBAR_ON_TOP - m_imgui->set_next_window_pos(x, y, ImGuiCond_Always, 0.5f, 0.0f); -#else - m_imgui->set_next_window_pos(x, y, ImGuiCond_Always, 1.0f, 0.0f); -#endif - - //BBS - ImGuiWrapper::push_toolbar_style(); - - 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 -#if ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT - m_imgui->set_requires_extra_frame(); -#else - m_parent.request_extra_frame(); -#endif // ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT - if (last_h != win_h) - last_h = win_h; - if (last_y != y) - last_y = y; - }*/ - - 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("Cut to parts"), m_cut_to_parts); // BBS - m_imgui->checkbox(_L("Rotate lower part upwards"), m_rotate_lower); - - // BBS - ImGui::Separator(); - m_imgui->checkbox(_L("Auto Segment"), m_do_segment); - m_imgui->disabled_begin(!m_do_segment); - ImGui::InputDouble("smoothing_alpha", &m_segment_smoothing_alpha, 0.0f, 0.0f, "%.2f"); - m_segment_smoothing_alpha = std::max(0.1, std::min(100.0, m_segment_smoothing_alpha)); - ImGui::InputInt("segment number", &m_segment_number); - m_segment_number = std::max(1, m_segment_number); - m_imgui->disabled_end(); - - ImGui::Separator(); - - m_imgui->disabled_begin((!m_keep_upper && !m_keep_lower && !m_do_segment) || 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(); - - //BBS - ImGuiWrapper::pop_toolbar_style(); - - // BBS: m_do_segment - if (cut_clicked && (m_keep_upper || m_keep_lower || m_do_segment)) - perform_cut(m_parent.get_selection()); -} - -void GLGizmoCut::set_cut_z(double cut_z) -{ - // Clamp the plane to the object's bounding box - m_cut_z = std::clamp(cut_z, 0.0, m_max_z); -} - -void GLGizmoCut::perform_cut(const Selection& selection) -{ - 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"); - - // m_cut_z is the distance from the bed. Subtract possible SLA elevation. - const GLVolume* first_glvolume = selection.get_volume(*selection.get_volume_idxs().begin()); - const double object_cut_z = m_cut_z - first_glvolume->get_sla_shift_z(); - - // BBS: do segment - if (m_do_segment) - { - wxGetApp().plater()->segment(object_idx, instance_idx, m_segment_smoothing_alpha, m_segment_number); + // render sphere grabber + size = m_dragging ? get_dragging_half_size(mean_size) : get_half_size(mean_size); + color = m_hover_id == Y ? complementary(ColorRGBA::GREEN()) : + m_hover_id == X ? complementary(ColorRGBA::RED()) : + m_hover_id == Z ? GRABBER_COLOR : ColorRGBA::GRAY(); + render_model(m_sphere.model, color, view_matrix * translation_transform(m_grabber_connection_len * Vec3d::UnitZ()) * scale_transform(size)); } - else if (0.0 < object_cut_z && object_cut_z < m_max_z) { - //wxGetApp().plater()->cut(object_idx, instance_idx, object_cut_z, m_keep_upper, m_keep_lower, m_rotate_lower, m_cut_to_parts); + + const bool no_xy_grabber_hovered = !m_dragging && (m_hover_id < 0 || m_hover_id == CutPlane); + + // render X grabber + + if (no_xy_grabber_hovered || m_hover_id == X) + { + size = m_dragging && m_hover_id == X ? get_dragging_half_size(mean_size) : get_half_size(mean_size); + const Vec3d 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); + } + + Vec3d offset = Vec3d(0.0, 1.25 * size, m_grabber_connection_len); + render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitX()) * scale_transform(cone_scale)); + offset = Vec3d(0.0, -1.25 * size, m_grabber_connection_len); + render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitX()) * scale_transform(cone_scale)); + } + + // render Y grabber + + if (no_xy_grabber_hovered || m_hover_id == Y) + { + size = m_dragging && m_hover_id == Y ? get_dragging_half_size(mean_size) : get_half_size(mean_size); + const Vec3d 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); + } + + Vec3d offset = Vec3d(1.25 * size, 0.0, m_grabber_connection_len); + render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitY()) * scale_transform(cone_scale)); + offset = Vec3d(-1.25 * size, 0.0, m_grabber_connection_len); + render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitY()) * scale_transform(cone_scale)); + } + + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { + + // render CutPlaneZRotation grabber + + if (no_xy_grabber_hovered || m_hover_id == CutPlaneZRotation) + { + size = 0.75 * (m_dragging ? get_dragging_half_size(mean_size) : get_half_size(mean_size)); + color = ColorRGBA::BLUE(); + const ColorRGBA cp_color = m_hover_id == CutPlaneZRotation ? color : m_plane.model.get_color(); + + const double grabber_shift = -1.75 * m_grabber_connection_len; + + render_model(m_sphere.model, cp_color, view_matrix * translation_transform(grabber_shift * Vec3d::UnitY()) * scale_transform(size)); + + if (m_hover_id == CutPlaneZRotation) { + const Vec3d cone_scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); + + render_rotation_snapping(CutPlaneZRotation, color); + render_grabber_connection(GRABBER_COLOR, view_matrix * rotation_transform(0.5 * PI * Vec3d::UnitX()), 1.75); + + Vec3d offset = Vec3d(1.25 * size, grabber_shift, 0.0); + render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitY()) * scale_transform(cone_scale)); + offset = Vec3d(-1.25 * size, grabber_shift, 0.0); + render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitY()) * scale_transform(cone_scale)); + } + } + + const double xy_connection_len = 0.75 * m_grabber_connection_len; + + // render CutPlaneXMove grabber + + if (no_xy_grabber_hovered || m_hover_id == CutPlaneXMove) + { + size = (m_dragging ? get_dragging_half_size(mean_size) : get_half_size(mean_size)); + color = m_hover_id == CutPlaneXMove ? ColorRGBA::RED() : m_plane.model.get_color(); + + render_grabber_connection(GRABBER_COLOR, view_matrix * rotation_transform(0.5 * PI * Vec3d::UnitY()), 0.75); + + Vec3d offset = xy_connection_len * Vec3d::UnitX() - 0.5 * size * Vec3d::Ones(); + render_model(m_cube.model, color, view_matrix * translation_transform(offset) * scale_transform(size)); + + const Vec3d cone_scale = Vec3d(0.5 * size, 0.5 * size, 1.8 * size); + + offset = (size + xy_connection_len) * Vec3d::UnitX(); + render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitY()) * scale_transform(cone_scale)); + } + + // render CutPlaneYMove grabber + + if (m_groove.angle > 0.0f && (no_xy_grabber_hovered || m_hover_id == CutPlaneYMove)) + { + size = (m_dragging ? get_dragging_half_size(mean_size) : get_half_size(mean_size)); + color = m_hover_id == CutPlaneYMove ? ColorRGBA::GREEN() : m_plane.model.get_color(); + + render_grabber_connection(GRABBER_COLOR, view_matrix * rotation_transform(-0.5 * PI * Vec3d::UnitX()), 0.75); + + Vec3d offset = xy_connection_len * Vec3d::UnitY() - 0.5 * size * Vec3d::Ones(); + render_model(m_cube.model, color, view_matrix * translation_transform(offset) * scale_transform(size)); + + const Vec3d cone_scale = Vec3d(0.5 * size, 0.5 * size, 1.8 * size); + + offset = (size + xy_connection_len) * Vec3d::UnitY(); + render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitX()) * scale_transform(cone_scale)); + } + } +} + +void GLGizmoCut3D::render_cut_line() +{ + if (!cut_line_processing() || m_line_end.isApprox(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) +{ + size_t mode; + float groove_depth; + float groove_width; + float groove_flaps_angle; + float groove_angle; + float groove_depth_tolerance; + float groove_width_tolerance; + + ar( m_keep_upper, m_keep_lower, m_rotate_lower, m_rotate_upper, m_hide_cut_plane, mode, m_connectors_editing, + m_ar_plane_center, m_rotation_m, + groove_depth, groove_width, groove_flaps_angle, groove_angle, groove_depth_tolerance, groove_width_tolerance); + + m_start_dragging_m = m_rotation_m; + + m_transformed_bounding_box = transformed_bounding_box(m_ar_plane_center, m_rotation_m); + set_center_pos(m_ar_plane_center); + + if (m_mode != mode) + switch_to_mode(mode); + else if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { + if (!is_approx(m_groove.depth , groove_depth) || + !is_approx(m_groove.width , groove_width) || + !is_approx(m_groove.flaps_angle , groove_flaps_angle) || + !is_approx(m_groove.angle , groove_angle) || + !is_approx(m_groove.depth_tolerance, groove_depth_tolerance) || + !is_approx(m_groove.width_tolerance, groove_width_tolerance) ) + { + m_groove.depth = groove_depth; + m_groove.width = groove_width; + m_groove.flaps_angle = groove_flaps_angle; + m_groove.angle = groove_angle; + m_groove.depth_tolerance= groove_depth_tolerance; + m_groove.width_tolerance= groove_width_tolerance; + update_plane_model(); + } + reset_cut_by_contours(); + } + + 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_ar_plane_center, m_start_dragging_m, + m_groove.depth, m_groove.width, m_groove.flaps_angle, m_groove.angle, m_groove.depth_tolerance, m_groove.width_tolerance); +} + +std::string GLGizmoCut3D::on_get_name() const +{ + return _u8L("Cut"); +} + +void GLGizmoCut3D::apply_color_clip_plane_colors() +{ + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) + m_parent.set_color_clip_plane_colors({ CUT_PLANE_DEF_COLOR , CUT_PLANE_DEF_COLOR }); + else + m_parent.set_color_clip_plane_colors({ UPPER_PART_COLOR , LOWER_PART_COLOR }); +} + +void GLGizmoCut3D::on_set_state() +{ + if (m_state == On) { + m_parent.set_use_color_clip_plane(true); + + update_bb(); + m_connectors_editing = !m_selected.empty(); + m_transformed_bounding_box = transformed_bounding_box(m_plane_center, m_rotation_m); + + // initiate archived values + m_ar_plane_center = m_plane_center; + m_start_dragging_m = m_rotation_m; + reset_cut_by_contours(); + + m_parent.request_extra_frame(); } else { - // the object is SLA-elevated and the plane is under it. + if (auto oc = m_c->object_clipper()) { + oc->set_behavior(true, true, 0.); + oc->release(); + } + m_selected.clear(); + m_parent.set_use_color_clip_plane(false); + //m_c->selection_info()->set_use_shift(false); + + // Make sure that the part selection data are released when the gizmo is closed. + // The CallAfter is needed because in perform_cut, the gizmo is closed BEFORE + // the cut is performed (because of undo/redo snapshots), so the data would + // be deleted prematurely. + if (m_part_selection.valid()) + wxGetApp().CallAfter([this]() { m_part_selection = PartSelection(); }); } } -double GLGizmoCut::calc_projection(const Linef3& mouse_ray) const +void GLGizmoCut3D::on_register_raycasters_for_picking() { - double projection = 0.0; + // assert(m_raycasters.empty()); + if (!m_raycasters.empty()) + on_unregister_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); - 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(); - // 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 + 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 if (!cut_line_processing()) { + 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::FallbackGizmo, CutPlane, *m_plane.mesh_raycaster, Transform3d::Identity())); + + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneZRotation, *m_sphere.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneZRotation, *m_cone.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneZRotation, *m_cone.mesh_raycaster, Transform3d::Identity())); + + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneXMove, *m_cube.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneXMove, *m_cone.mesh_raycaster, Transform3d::Identity())); + + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneYMove, *m_cube.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneYMove, *m_cone.mesh_raycaster, Transform3d::Identity())); + } + } + + update_raycasters_for_picking_transform(); +} + +void GLGizmoCut3D::on_unregister_raycasters_for_picking() +{ + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo); + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::FallbackGizmo); + 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); +} + +void GLGizmoCut3D::update_raycasters_for_picking() +{ + on_unregister_raycasters_for_picking(); + on_register_raycasters_for_picking(); +} + +void GLGizmoCut3D::set_volumes_picking_state(bool state) +{ + 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); + } + } +} + +void GLGizmoCut3D::update_raycasters_for_picking_transform() +{ + 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; + + const Vec3d& instance_offset = mo->instances[inst_id]->get_offset(); + const double sla_shift = double(m_c->selection_info()->get_sla_shift()); + + const bool looking_forward = is_looking_forward(); + + 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; + if (connector.attribs.type == CutConnectorType::Dowel && + connector.attribs.style == CutConnectorStyle::Prism) { + height = 0.05f; + if (!looking_forward) + pos += 0.05 * m_clp_normal; + } + 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 if (!cut_line_processing()){ + const Transform3d trafo = translation_transform(m_plane_center) * m_rotation_m; + + const BoundingBoxf3 box = m_bounding_box; + + const double size = get_half_size(get_grabber_mean_size(box)); + Vec3d scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); + + int id = 0; + + Vec3d offset = Vec3d(0.0, 1.25 * size, m_grabber_connection_len); + m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitX()) * scale_transform(scale)); + offset = Vec3d(0.0, -1.25 * size, m_grabber_connection_len); + m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitX()) * scale_transform(scale)); + + offset = Vec3d(1.25 * size, 0.0, m_grabber_connection_len); + m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitY()) * scale_transform(scale)); + offset = Vec3d(-1.25 * size, 0.0, m_grabber_connection_len); + m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitY()) * scale_transform(scale)); + + m_raycasters[id++]->set_transform(trafo * translation_transform(m_grabber_connection_len * Vec3d::UnitZ()) * scale_transform(size)); + + m_raycasters[id++]->set_transform(trafo); + + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { + + double grabber_y_shift = -1.75 * m_grabber_connection_len; + + m_raycasters[id++]->set_transform(trafo * translation_transform(grabber_y_shift * Vec3d::UnitY()) * scale_transform(size)); + + offset = Vec3d(1.25 * size, grabber_y_shift, 0.0); + m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitY()) * scale_transform(scale)); + offset = Vec3d(-1.25 * size, grabber_y_shift, 0.0); + m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitY()) * scale_transform(scale)); + + const double xy_connection_len = 0.75 * m_grabber_connection_len; + const Vec3d cone_scale = Vec3d(0.5 * size, 0.5 * size, 1.8 * size); + + offset = xy_connection_len * Vec3d::UnitX() - 0.5 * size * Vec3d::Ones(); + m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * scale_transform(size)); + offset = (size + xy_connection_len) * Vec3d::UnitX(); + m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitY()) * scale_transform(cone_scale)); + + if (m_groove.angle > 0.0f) { + offset = xy_connection_len * Vec3d::UnitY() - 0.5 * size * Vec3d::Ones(); + m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * scale_transform(size)); + offset = (size + xy_connection_len) * Vec3d::UnitY(); + m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitX()) * scale_transform(cone_scale)); + } + else { + // discard transformation for CutPlaneYMove grabbers + m_raycasters[id++]->set_transform(Transform3d::Identity()); + m_raycasters[id++]->set_transform(Transform3d::Identity()); + } + } + } +} + +void GLGizmoCut3D::update_plane_model() +{ + m_plane.reset(); + on_unregister_raycasters_for_picking(); + + init_picking_models(); +} + +void GLGizmoCut3D::on_set_hover_id() +{ +} + +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 || selection.is_wipe_tower()) + return false; + + if (const ModelObject* mo = wxGetApp().plater()->model().objects[object_idx]; + mo->is_cut() && mo->volumes.size() == 1) { + const ModelVolume* volume = mo->volumes[0]; + if (volume->is_cut_connector() && volume->cut_info.connector_type == CutConnectorType::Dowel) + return false; + } + + // This is assumed in GLCanvas3D::do_rotate, do not change this + // without updating that function too. + return selection.is_single_full_instance() && !m_parent.is_layers_editing_enabled(); +} + +bool GLGizmoCut3D::on_is_selectable() const +{ + return wxGetApp().get_mode() != comSimple; +} + +Vec3d GLGizmoCut3D::mouse_position_in_local_plane(GrabberID 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; + } + case Z: + default: + { + // 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_move(const GLGizmoBase::UpdateData &data) +{ + Vec3d starting_drag_position; + if (m_hover_id == Z) + starting_drag_position = translation_transform(m_plane_center) * m_rotation_m * (m_grabber_connection_len * Vec3d::UnitZ()); + else + starting_drag_position = m_cut_plane_start_move_pos; + + double projection = 0.0; + + Vec3d starting_vec = m_rotation_m * (m_hover_id == CutPlaneXMove ? Vec3d::UnitX() : m_hover_id == CutPlaneYMove ? Vec3d::UnitY() : Vec3d::UnitZ()); + if (starting_vec.norm() != 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 through the starting position + // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebraic 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; + const Vec3d inters = data.mouse_ray.a + (starting_drag_position - data.mouse_ray.a).dot(mouse_dir) * mouse_dir; // vector from the starting position to the found intersection - const Vec3d inters_vec = inters - m_drag_pos; + const 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 * std::round(projection / m_snap_step); + + const Vec3d shift = starting_vec * projection; + if (shift != Vec3d::Zero()) + reset_cut_by_contours(); + + // move cut plane center + set_center(m_plane_center + shift, true); + + m_was_cut_plane_dragged = true; } -BoundingBoxf3 GLGizmoCut::bounding_box() const +void GLGizmoCut3D::dragging_grabber_rotation(const GLGizmoBase::UpdateData &data) +{ + const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane((GrabberID)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 != Y) + theta += 0.5 * PI; + + if (!is_approx(theta, 0.0)) + reset_cut_by_contours(); + + Vec3d rotation = Vec3d::Zero(); + rotation[m_hover_id == CutPlaneZRotation ? Z : m_hover_id] = theta; + + const Transform3d rotation_tmp = m_start_dragging_m * rotation_transform(rotation); + const bool update_tbb = !m_rotation_m.rotation().isApprox(rotation_tmp.rotation()); + m_rotation_m = rotation_tmp; + if (update_tbb) + m_transformed_bounding_box = transformed_bounding_box(m_plane_center, m_rotation_m); + + 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; + Vec3d pos; + Vec3d pos_world; + + if (unproject_on_cut_plane(data.mouse_pos.cast(), pos, pos_world)) { + connectors[m_hover_id - m_connectors_group_id].pos = pos; + update_raycasters_for_picking_transform(); + } +} + +void GLGizmoCut3D::on_dragging(const UpdateData& data) +{ + if (m_hover_id < 0) + return; + if (m_hover_id == Z || m_hover_id == CutPlane || m_hover_id == CutPlaneXMove || m_hover_id == CutPlaneYMove) + dragging_grabber_move(data); + else if (m_hover_id == X || m_hover_id == Y || m_hover_id == CutPlaneZRotation) + dragging_grabber_rotation(data); + else if (m_hover_id >= m_connectors_group_id && m_connector_mode == CutConnectorMode::Manual) + dragging_connector(data); + check_and_update_connectors_state(); + + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) + reset_cut_by_contours(); +} + +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(), _u8L("Move connector"), UndoRedo::SnapshotType::GizmoAction); + + if (m_hover_id == X || m_hover_id == Y || m_hover_id == CutPlaneZRotation) + m_start_dragging_m = m_rotation_m; +} + +void GLGizmoCut3D::on_stop_dragging() +{ + if (m_hover_id == X || m_hover_id == Y || m_hover_id == CutPlaneZRotation) { + m_angle_arc.reset(); + m_angle = 0.0; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Rotate cut plane"), UndoRedo::SnapshotType::GizmoAction); + m_start_dragging_m = m_rotation_m; + } + else if (m_hover_id == Z || m_hover_id == CutPlane || m_hover_id == CutPlaneXMove|| m_hover_id == CutPlaneYMove) { + if (m_was_cut_plane_dragged) + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Move cut plane"), UndoRedo::SnapshotType::GizmoAction); + m_ar_plane_center = m_plane_center; + } + + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) + reset_cut_by_contours(); + //check_and_update_connectors_state(); +} + +void GLGizmoCut3D::set_center_pos(const Vec3d& center_pos, bool update_tbb /*=false*/) +{ + BoundingBoxf3 tbb = m_transformed_bounding_box; + if (update_tbb) { + Vec3d normal = m_rotation_m.inverse() * Vec3d(m_plane_center - center_pos); + tbb.translate(normal.z() * Vec3d::UnitZ()); + } + + bool can_set_center_pos = false; + { + double limit_val = /*CutMode(m_mode) == CutMode::cutTongueAndGroove ? 0.5 * double(m_groove.depth) : */0.5; + if (tbb.max.z() > -limit_val && tbb.min.z() < limit_val) + 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_transformed_bounding_box = tbb; + 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(); 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, const Transform3d& rotation_m/* = Transform3d::Identity()*/) const { const Selection& selection = m_parent.get_selection(); - const GLVolume* first_glvolume = selection.get_volume(*selection.get_volume_idxs().begin()); - const BoundingBoxf3& box = first_glvolume->transformed_convex_hull_bounding_box(); - const ModelObject* model_object = wxGetApp().model().objects[selection.get_object_idx()]; - const int instance_idx = selection.get_instance_idx(); + const auto first_volume = selection.get_first_volume(); + Vec3d instance_offset = first_volume->get_instance_offset(); + instance_offset[Z] += first_volume->get_sla_shift_z(); - 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.cut_z = m_cut_z; + const auto cut_matrix = Transform3d::Identity() * rotation_m.inverse() * translation_transform(instance_offset - plane_center); - if (m_cut_contours.object_id != model_object->id()) - m_cut_contours.mesh = model_object->raw_mesh(); + const Selection::IndicesList& idxs = selection.get_volume_idxs(); + BoundingBoxf3 ret; + 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()) { - 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.contours.reset(); - - MeshSlicingParams slicing_params; - slicing_params.trafo = first_glvolume->get_instance_transformation().get_matrix(); - 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)); - m_cut_contours.contours.set_color(-1, { 1.0f, 1.0f, 1.0f, 1.0f }); - } - } - else if (box.center() != m_cut_contours.position) { - m_cut_contours.shift = box.center() - m_cut_contours.position; + const auto instance_matrix = volume->get_instance_transformation().get_matrix(true); + auto volume_trafo = instance_matrix * volume->get_volume_transformation().get_matrix(); + ret.merge(volume->transformed_convex_hull_bounding_box(cut_matrix * volume_trafo)); } } - else - m_cut_contours.contours.reset(); + return ret; } +void GLGizmoCut3D::update_bb() +{ + const BoundingBoxf3 box = bounding_box(); + if (!box.defined) + return; + if (!m_max_pos.isApprox(box.max) || !m_min_pos.isApprox(box.min)) { + + m_bounding_box = box; + + // check, if mode is set to Planar, when object has a connectors + if (const int object_idx = m_parent.get_selection().get_object_idx(); + object_idx >= 0 && !wxGetApp().plater()->model().objects[object_idx]->cut_connectors.empty()) + m_mode = size_t(CutMode::cutPlanar); + + invalidate_cut_plane(); + reset_cut_by_contours(); + apply_color_clip_plane_colors(); + + m_max_pos = box.max; + m_min_pos = box.min; + m_bb_center = box.center(); + m_transformed_bounding_box = transformed_bounding_box(m_bb_center); + if (box.contains(m_center_offset)) + set_center_pos(m_bb_center + m_center_offset); + else + set_center_pos(m_bb_center); + + m_contour_width = CutMode(m_mode) == CutMode::cutTongueAndGroove ? 0.f : 0.4f; + + m_radius = box.radius(); + m_grabber_connection_len = 0.5 * 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; + + // input params for cut with tongue and groove + m_groove.depth = m_groove.depth_init = std::max(1.f , 0.5f * float(get_grabber_mean_size(m_bounding_box))); + m_groove.width = m_groove.width_init = 4.0f * m_groove.depth; + m_groove.flaps_angle = m_groove.flaps_angle_init = float(PI) / 3.f; + m_groove.angle = m_groove.angle_init = 0.f; + m_plane.reset(); + m_cone.reset(); + m_sphere.reset(); + m_cube.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(); + selection && selection->model_object()) + m_selected.resize(selection->model_object()->cut_connectors.size(), 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_cube.model.is_initialized()) { + indexed_triangle_set its = its_make_cube(1., 1., 1.); + m_cube.model.init_from(its); + m_cube.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); + } + + if (!m_plane.model.is_initialized() && !m_hide_cut_plane && !m_connectors_editing) { + const double cp_width = 0.02 * get_grabber_mean_size(m_bounding_box); + indexed_triangle_set its = m_mode == size_t(CutMode::cutTongueAndGroove) ? its_make_groove_plane() : + its_make_frustum_dowel((double)m_cut_plane_radius_koef * m_radius, cp_width, m_cut_plane_as_circle ? 180 : 4); + + m_plane.model.init_from(its); + m_plane.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); +} + +void GLGizmoCut3D::render_clipper_cut() +{ + if (! m_connectors_editing) + ::glDisable(GL_DEPTH_TEST); + + GLboolean cull_face = GL_FALSE; + ::glGetBooleanv(GL_CULL_FACE, &cull_face); + ::glDisable(GL_CULL_FACE); + m_c->object_clipper()->render_cut(m_part_selection.get_ignored_contours_ptr()); + if (cull_face) + ::glEnable(GL_CULL_FACE); + + if (! m_connectors_editing) + ::glEnable(GL_DEPTH_TEST); +} + +void GLGizmoCut3D::PartSelection::add_object(const ModelObject* object) +{ + m_model = Model(); + m_model.add_object(*object); + + const double sla_shift_z = wxGetApp().plater()->canvas3D()->get_selection().get_first_volume()->get_sla_shift_z(); + if (!is_approx(sla_shift_z, 0.)) { + Vec3d inst_offset = model_object()->instances[m_instance_idx]->get_offset(); + inst_offset[Z] += sla_shift_z; + model_object()->instances[m_instance_idx]->set_offset(inst_offset); + } +} + + +GLGizmoCut3D::PartSelection::PartSelection(const ModelObject* mo, const Transform3d& cut_matrix, int instance_idx_in, const Vec3d& center, const Vec3d& normal, const CommonGizmosDataObjects::ObjectClipper& oc) + : m_instance_idx(instance_idx_in) +{ + Cut cut(mo, instance_idx_in, cut_matrix); + add_object(cut.perform_with_plane().front()); + + const ModelVolumePtrs& volumes = model_object()->volumes; + + // split to parts + for (int id = int(volumes.size())-1; id >= 0; id--) + if (volumes[id]->is_splittable()) + volumes[id]->split(1); + + m_parts.clear(); + for (const ModelVolume* volume : volumes) { + assert(volume != nullptr); + m_parts.emplace_back(Part{GLModel(), MeshRaycaster(volume->mesh()), true, !volume->is_model_part()}); + m_parts.back().glmodel.set_color({ 0.f, 0.f, 1.f, 1.f }); + m_parts.back().glmodel.init_from(volume->mesh()); + + // Now check whether this part is below or above the plane. + Transform3d tr = (model_object()->instances[m_instance_idx]->get_matrix() * volume->get_matrix()).inverse(); + Vec3f pos = (tr * center).cast(); + Vec3f norm = (tr.linear().inverse().transpose() * normal).cast(); + for (const Vec3f& v : volume->mesh().its.vertices) { + double p = (v - pos).dot(norm); + if (std::abs(p) > EPSILON) { + m_parts.back().selected = p > 0.; + break; + } + } + } + + // Now go through the contours and create a map from contours to parts. + m_contour_points.clear(); + m_contour_to_parts.clear(); + m_debug_pts = std::vector>(m_parts.size(), std::vector()); + if (std::vector pts = oc.point_per_contour();! pts.empty()) { + + m_contour_to_parts.resize(pts.size()); + + for (size_t pt_idx=0; pt_idxinstances[m_instance_idx]->get_offset()) * translation_transform(model_object()->volumes[part_id]->get_offset())).inverse(); + for (double d : {-1., 1.}) { + const Vec3d dir_mesh = d * tr.linear().inverse().transpose() * normal; + const Vec3d src = tr * (m_contour_points[pt_idx] + d*0.01 * normal); + AABBMesh::hit_result hit = aabb.query_ray_hit(src, dir_mesh); + + m_debug_pts[part_id].emplace_back(src); + + if (hit.is_inside()) { + // This part belongs to this point. + if (d == 1.) + m_contour_to_parts[pt_idx].first.emplace_back(part_id); + else + m_contour_to_parts[pt_idx].second.emplace_back(part_id); + } + } + } + } + + } + + + m_valid = true; +} + +// In CutMode::cutTongueAndGroove we use PartSelection just for rendering +GLGizmoCut3D::PartSelection::PartSelection(const ModelObject* object, int instance_idx_in) + : m_instance_idx (instance_idx_in) +{ + add_object(object); + + m_parts.clear(); + + for (const ModelVolume* volume : object->volumes) { + assert(volume != nullptr); + m_parts.emplace_back(Part{ GLModel(), MeshRaycaster(volume->mesh()), true, !volume->is_model_part() }); + m_parts.back().glmodel.init_from(volume->mesh()); + + // Now check whether this part is below or above the plane. + m_parts.back().selected = volume->is_from_upper(); + } + + m_valid = true; +} + +void GLGizmoCut3D::PartSelection::render(const Vec3d* normal, GLModel& sphere_model) +{ + if (! valid()) + return; + + const Camera& camera = wxGetApp().plater()->get_camera(); + + if (GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light")) { + shader->start_using(); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + shader->set_uniform("emission_factor", 0.f); + + // FIXME: Cache the transforms. + + const Vec3d inst_offset = model_object()->instances[m_instance_idx]->get_offset(); + const Transform3d view_inst_matrix= camera.get_view_matrix() * translation_transform(inst_offset); + + const bool is_looking_forward = normal && camera.get_dir_forward().dot(*normal) < 0.05; + + for (size_t id=0; idset_uniform("view_model_matrix", view_inst_matrix * model_object()->volumes[id]->get_matrix()); + if (m_parts[id].is_modifier) { + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + } + m_parts[id].glmodel.set_color(m_parts[id].is_modifier ? MODIFIER_COLOR : (m_parts[id].selected ? UPPER_PART_COLOR : LOWER_PART_COLOR)); + m_parts[id].glmodel.render(); + if (m_parts[id].is_modifier) + glsafe(::glDisable(GL_BLEND)); + } + + shader->stop_using(); + } + + + + // { // Debugging render: + + // static int idx = -1; + // ImGui::Begin("DEBUG"); + // for (int i=0; i= m_parts.size()) + // idx = -1; + // ImGui::End(); + + // ::glDisable(GL_DEPTH_TEST); + // if (valid()) { + // for (size_t i=0; i GLGizmoCut3D::PartSelection::get_cut_parts() +{ + std::vector parts; + + for (const auto& part : m_parts) + parts.push_back({part.selected, part.is_modifier}); + + return parts; +} + + +void GLGizmoCut3D::PartSelection::toggle_selection(const Vec2d& mouse_pos) +{ + // FIXME: Cache the transforms. + const Camera& camera = wxGetApp().plater()->get_camera(); + const Vec3d& camera_pos = camera.get_position(); + + Vec3f pos; + Vec3f normal; + + std::vector> hits_id_and_sqdist; + + for (size_t id=0; idvolumes[id]->get_offset(); + Transform3d tr = translation_transform(model_object()->instances[m_instance_idx]->get_offset()) * translation_transform(model_object()->volumes[id]->get_offset()); + if (m_parts[id].raycaster.unproject_on_mesh(mouse_pos, tr, camera, pos, normal)) { + hits_id_and_sqdist.emplace_back(id, (camera_pos - tr*(pos.cast())).squaredNorm()); + } + } + if (! hits_id_and_sqdist.empty()) { + size_t id = std::min_element(hits_id_and_sqdist.begin(), hits_id_and_sqdist.end(), + [](const std::pair& a, const std::pair& b) { return a.second < b.second; })->first; + m_parts[id].selected = ! m_parts[id].selected; + + // And now recalculate the contours which should be ignored. + m_ignored_contours.clear(); + size_t cont_id = 0; + for (const auto& [parts_above, parts_below] : m_contour_to_parts) { + for (size_t upper : parts_above) { + bool upper_sel = m_parts[upper].selected; + if (std::find_if(parts_below.begin(), parts_below.end(), [this, &upper_sel](const size_t& i) { return m_parts[i].selected == upper_sel; }) != parts_below.end()) { + m_ignored_contours.emplace_back(cont_id); + break; + } + } + ++cont_id; + } + } +} + +void GLGizmoCut3D::PartSelection::turn_over_selection() +{ + for (Part& part : m_parts) + part.selected = !part.selected; +} + +void GLGizmoCut3D::on_render() +{ + if (m_state == On) { + // This gizmo is showing the object elevated. Tell the common + // SelectionInfo object to lie about the actual shift. + //m_c->selection_info()->set_use_shift(true); + } + + // check objects visibility + toggle_model_objects_visibility(); + + update_clipper(); + + init_picking_models(); + + init_rendering_items(); + + render_connectors(); + + if (!m_connectors_editing) + m_part_selection.render(nullptr, m_sphere.model); + else + m_part_selection.render(&m_cut_normal, m_sphere.model); + + render_clipper_cut(); + + if (!m_hide_cut_plane && !m_connectors_editing) { + render_cut_plane(); + render_cut_plane_grabbers(); + } + + render_cut_line(); + + m_selection_rectangle.render(m_parent); +} + +void GLGizmoCut3D::render_debug_input_window(float x) +{ + return; + m_imgui->begin(wxString("DEBUG")); + + m_imgui->end(); +/* + 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::PushItemWidth(0.5f * m_label_width); + if (auto oc = m_c->object_clipper(); oc && m_imgui->slider_float("contour_width", &m_contour_width, 0.f, 3.f)) + oc->set_behavior(m_connectors_editing, m_connectors_editing, double(m_contour_width)); + + ImGui::Separator(); + + if (m_imgui->checkbox(("Render cut plane as disc"), m_cut_plane_as_circle)) + m_plane.reset(); + + ImGui::PushItemWidth(0.5f * m_label_width); + if (m_imgui->slider_float("cut_plane_radius_koef", &m_cut_plane_radius_koef, 1.f, 2.f)) + 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_shortcut_label_width < 0.f) { + for (const auto& shortcut : m_shortcuts) { + const float width = m_imgui->calc_text_size(shortcut.first).x; + if (m_shortcut_label_width < width) + m_shortcut_label_width = width; + } + m_shortcut_label_width += +m_imgui->scaled(1.f); + } + + if (m_show_shortcuts) + for (const auto&shortcut : m_shortcuts ){ + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, shortcut.first); + ImGui::SameLine(m_shortcut_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); + check_and_update_connectors_state(); + 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, m_labels_map["Connectors"]); + + m_imgui->disabled_begin(connectors.empty()); + ImGui::SameLine(m_label_width); + const std::string act_name = _u8L("Remove connectors"); + if (render_reset_button("connectors", act_name)) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), act_name, UndoRedo::SnapshotType::GizmoAction); + reset_connectors(); + } + m_imgui->disabled_end(); + + render_flip_plane_button(m_connectors_editing && connectors.empty()); + + m_imgui->text(m_labels_map["Type"]); + bool type_changed = render_connect_type_radio_button(CutConnectorType::Plug); + type_changed |= render_connect_type_radio_button(CutConnectorType::Dowel); + type_changed |= render_connect_type_radio_button(CutConnectorType::Snap); + 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::Plug); + if (type_changed && m_connector_type == CutConnectorType::Dowel) { + m_connector_style = int(CutConnectorStyle::Prism); + apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.style = CutConnectorStyle(m_connector_style); }); + } + if (render_combo(m_labels_map["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(); + + m_imgui->disabled_begin(m_connector_type == CutConnectorType::Snap); + if (type_changed && m_connector_type == CutConnectorType::Snap) { + m_connector_shape_id = int(CutConnectorShape::Circle); + apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.shape = CutConnectorShape(m_connector_shape_id); }); + } + if (render_combo(m_labels_map["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); }); + m_imgui->disabled_end(); + + const float depth_min_value = m_connector_type == CutConnectorType::Snap ? m_connector_size : -0.1f; + if (render_slider_double_input(m_labels_map["Depth"], m_connector_depth_ratio, m_connector_depth_ratio_tolerance, depth_min_value)) + 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(m_labels_map["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 = 0.5f * m_connector_size_tolerance; + }); + + if (render_angle_input(m_labels_map["Rotation"], m_connector_angle, 0.f, 0.f, 180.f)) + apply_selected_connectors([this, &connectors](size_t idx) { + connectors[idx].z_angle = m_connector_angle; + }); + + if (m_connector_type == CutConnectorType::Snap) { + render_snap_specific_input(_u8L("Bulge"), _L("Bulge proportion related to radius"), m_snap_bulge_proportion, 0.15f, 5.f, 100.f * m_snap_space_proportion); + render_snap_specific_input(_u8L("Space"), _L("Space proportion related to radius"), m_snap_space_proportion, 0.3f, 10.f, 50.f); + } + + ImGui::Separator(); + + if (m_imgui->button(_L("Confirm connectors"))) { + unselect_all_connectors(); + set_connectors_editing(false); + } + + ImGui::SameLine(m_label_width + 1.15f * m_control_width); + + if (m_imgui->button(_L("Cancel"))) { + reset_connectors(); + set_connectors_editing(false); + } +} + +void GLGizmoCut3D::render_build_size() +{ + double koef = m_imperial_units ? GizmoObjectManipulation::mm_to_in : 1.0; + wxString unit_str = " " + (m_imperial_units ? _L("in") : _L("mm")); + + Vec3d tbb_sz = m_transformed_bounding_box.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 Volume")); + ImGui::SameLine(); + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, size); +} + +void GLGizmoCut3D::reset_cut_plane() +{ + m_angle_arc.reset(); + m_transformed_bounding_box = transformed_bounding_box(m_bb_center); + set_center(m_bb_center); + m_start_dragging_m = m_rotation_m = Transform3d::Identity(); + m_ar_plane_center = m_plane_center; + + reset_cut_by_contours(); + m_parent.request_extra_frame(); +} + +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) +{ + if (m_connectors_editing == connectors_editing) + return; + + m_connectors_editing = connectors_editing; + update_raycasters_for_picking(); + + m_c->object_clipper()->set_behavior(m_connectors_editing, m_connectors_editing, double(m_contour_width)); + + m_parent.request_extra_frame(); +} + +void GLGizmoCut3D::flip_cut_plane() +{ + m_rotation_m = m_rotation_m * rotation_transform(PI * Vec3d::UnitX()); + m_transformed_bounding_box = transformed_bounding_box(m_plane_center, m_rotation_m); + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Flip cut plane"), UndoRedo::SnapshotType::GizmoAction); + m_start_dragging_m = m_rotation_m; + + update_clipper(); + m_part_selection.turn_over_selection(); + + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) + reset_cut_by_contours(); +} + +void GLGizmoCut3D::reset_cut_by_contours() +{ + m_part_selection = PartSelection(); + + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { + if (m_dragging || m_groove_editing || !has_valid_groove()) + return; + process_contours(); + } + else + toggle_model_objects_visibility(); +} + +void GLGizmoCut3D::process_contours() +{ + const Selection& selection = m_parent.get_selection(); + const ModelObjectPtrs& model_objects = selection.get_model()->objects; + + const int instance_idx = selection.get_instance_idx(); + if (instance_idx < 0) + return; + const int object_idx = selection.get_object_idx(); + + wxBusyCursor wait; + + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { + if (has_valid_groove()) { + Cut cut(model_objects[object_idx], instance_idx, get_cut_matrix(selection)); + const ModelObjectPtrs& new_objects = cut.perform_with_groove(m_groove, m_rotation_m, true); + if (!new_objects.empty()) + m_part_selection = PartSelection(new_objects.front(), instance_idx); + } + } + else { + reset_cut_by_contours(); + m_part_selection = PartSelection(model_objects[object_idx], get_cut_matrix(selection), instance_idx, m_plane_center, m_cut_normal, *m_c->object_clipper()); + } + + toggle_model_objects_visibility(); +} + +void GLGizmoCut3D::render_flip_plane_button(bool disable_pred /*=false*/) +{ + ImGui::SameLine(); + + if (m_hover_id == CutPlane) + ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetColorU32(ImGuiCol_ButtonHovered)); + + m_imgui->disabled_begin(disable_pred); + if (m_imgui->button(_L("Flip cut plane"))) + flip_cut_plane(); + m_imgui->disabled_end(); + + if (m_hover_id == CutPlane) + ImGui::PopStyleColor(); +} + +void GLGizmoCut3D::add_vertical_scaled_interval(float interval) +{ + ImGui::GetCurrentWindow()->DC.CursorPos.y += m_imgui->scaled(interval); +} + +void GLGizmoCut3D::add_horizontal_scaled_interval(float interval) +{ + ImGui::GetCurrentWindow()->DC.CursorPos.x += m_imgui->scaled(interval); +} + +void GLGizmoCut3D::add_horizontal_shift(float shift) +{ + ImGui::GetCurrentWindow()->DC.CursorPos.x += shift; +} + +void GLGizmoCut3D::render_color_marker(float size, const ImU32& color) +{ + ImGui::SameLine(); + const float radius = 0.5f * size; + ImVec2 pos = ImGui::GetCurrentWindow()->DC.CursorPos; + pos.x += size; + pos.y += 1.25f * radius; + ImGui::GetCurrentWindow()->DrawList->AddNgonFilled(pos, radius, color, 6); + m_imgui->text(" "); +} + +void GLGizmoCut3D::render_groove_float_input(const std::string& label, float& in_val, const float& init_val, float& in_tolerance) +{ + bool is_changed{false}; + + float val = in_val; + float tolerance = in_tolerance; + if (render_slider_double_input(label, val, tolerance, -0.1f, std::min(0.3f*in_val, 1.5f))) { + if (m_imgui->get_last_slider_status().can_take_snapshot) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), GUI::format("%1%: %2%", _u8L("Groove change"), label), UndoRedo::SnapshotType::GizmoAction); + m_imgui->get_last_slider_status().invalidate_snapshot(); + m_groove_editing = true; + } + in_val = val; + in_tolerance = tolerance; + is_changed = true; + } + + ImGui::SameLine(); + + m_imgui->disabled_begin(is_approx(in_val, init_val) && is_approx(in_tolerance, 0.1f)); + const std::string act_name = _u8L("Reset"); + if (render_reset_button(("##groove_" + label + act_name).c_str(), act_name)) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), GUI::format("%1%: %2%", act_name, label), UndoRedo::SnapshotType::GizmoAction); + in_val = init_val; + in_tolerance = 0.1f; + is_changed = true; + } + m_imgui->disabled_end(); + + if (is_changed) { + update_plane_model(); + reset_cut_by_contours(); + } + + if (m_is_slider_editing_done) { + m_groove_editing = false; + reset_cut_by_contours(); + } +} + +bool GLGizmoCut3D::render_angle_input(const std::string& label, float& in_val, const float& init_val, float min_val, float max_val) +{ + bool is_changed{ false }; + + m_imgui->text(label); + + ImGui::SameLine(m_label_width); + ImGui::PushItemWidth(m_control_width * 0.7f); + + float val = rad2deg(in_val); + const float old_val = val; + + const std::string format = "%.0f " + _u8L("°"); + m_imgui->slider_float(("##angle_" + label).c_str(), &val, min_val, max_val, format.c_str(), 1.f, true, from_u8(label)); + + m_is_slider_editing_done |= m_imgui->get_last_slider_status().deactivated_after_edit; + if (!is_approx(old_val, val)) { + if (m_imgui->get_last_slider_status().can_take_snapshot) { + // TRN: This is an entry in the Undo/Redo stack. The whole line will be 'Edited: (name of whatever was edited)'. + Plater::TakeSnapshot snapshot(wxGetApp().plater(), GUI::format("%1%: %2%", _L("Edited"), label), UndoRedo::SnapshotType::GizmoAction); + m_imgui->get_last_slider_status().invalidate_snapshot(); + if (m_mode == size_t(CutMode::cutTongueAndGroove)) + m_groove_editing = true; + } + in_val = deg2rad(val); + is_changed = true; + } + + ImGui::SameLine(); + + m_imgui->disabled_begin(is_approx(in_val, init_val)); + const std::string act_name = _u8L("Reset"); + if (render_reset_button(("##angle_" + label + act_name).c_str(), act_name)) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), GUI::format("%1%: %2%", act_name, label), UndoRedo::SnapshotType::GizmoAction); + in_val = init_val; + is_changed = true; + } + m_imgui->disabled_end(); + + return is_changed; +} + +void GLGizmoCut3D::render_groove_angle_input(const std::string& label, float& in_val, const float& init_val, float min_val, float max_val) +{ + if (render_angle_input(label, in_val, init_val, min_val, max_val)) { + update_plane_model(); + reset_cut_by_contours(); + } + + if (m_is_slider_editing_done) { + m_groove_editing = false; + reset_cut_by_contours(); + } +} + +void GLGizmoCut3D::render_snap_specific_input(const std::string& label, const wxString& tooltip, float& in_val, const float& init_val, const float min_val, const float max_val) +{ + m_imgui->text(label); + + ImGui::SameLine(m_label_width); + ImGui::PushItemWidth(m_control_width * 0.7f); + + bool is_changed = false; + const std::string format = "%.0f %%"; + + float val = in_val * 100.f; + if (m_imgui->slider_float(("##snap_" + label).c_str(), &val, min_val, max_val, format.c_str(), 1.f, true, tooltip)) { + in_val = val * 0.01f; + is_changed = true; + } + + ImGui::SameLine(); + + m_imgui->disabled_begin(is_approx(in_val, init_val)); + const std::string act_name = _u8L("Reset"); + if (render_reset_button(("##snap_" + label + act_name).c_str(), act_name)) { + in_val = init_val; + is_changed = true; + } + m_imgui->disabled_end(); + + if (is_changed) { + update_connector_shape(); + update_raycasters_for_picking(); + } +} + +void GLGizmoCut3D::render_cut_plane_input_window(CutConnectors &connectors) +{ +// if (m_mode == size_t(CutMode::cutPlanar)) { + CutMode mode = CutMode(m_mode); + if (mode == CutMode::cutPlanar || mode == CutMode::cutTongueAndGroove) { + ImGui::AlignTextToFramePadding(); + m_imgui->text(wxString(ImGui::InfoMarkerSmall)); + ImGui::SameLine(); + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, + get_wraped_wxString(_L("Hold SHIFT key to draw a cut line"), 40)); + ImGui::Separator(); + + const bool has_connectors = !connectors.empty(); + + m_imgui->disabled_begin(has_connectors); + if (render_cut_mode_combo()) + mode = CutMode(m_mode); + m_imgui->disabled_end(); + + render_build_size(); + + ImGui::AlignTextToFramePadding(); + m_imgui->text(_L("Cut position") + ": "); + ImGui::SameLine(); + render_move_center_input(Z); + ImGui::SameLine(); + + const bool is_cut_plane_init = m_rotation_m.isApprox(Transform3d::Identity()) && m_bb_center.isApprox(m_plane_center); + m_imgui->disabled_begin(is_cut_plane_init); + std::string act_name = _u8L("Reset cutting plane"); + if (render_reset_button("cut_plane", into_u8(act_name))) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), act_name, UndoRedo::SnapshotType::GizmoAction); + reset_cut_plane(); + } + m_imgui->disabled_end(); + +// render_flip_plane_button(); + + if (mode == CutMode::cutPlanar) { + add_vertical_scaled_interval(0.75f); + + m_imgui->disabled_begin(!m_keep_upper || !m_keep_lower || m_keep_as_parts || (m_part_selection.valid() && m_part_selection.is_one_object())); + if (m_imgui->button(has_connectors ? _L("Edit connectors") : _L("Add connectors"))) + set_connectors_editing(true); + m_imgui->disabled_end(); + + ImGui::SameLine(1.5f * m_control_width); + + m_imgui->disabled_begin(is_cut_plane_init && !has_connectors); + act_name = _u8L("Reset cut"); + if (m_imgui->button(act_name, _u8L("Reset cutting plane and remove connectors"))) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), act_name, UndoRedo::SnapshotType::GizmoAction); + reset_cut_plane(); + reset_connectors(); + } + m_imgui->disabled_end(); + } + else if (mode == CutMode::cutTongueAndGroove) { + m_is_slider_editing_done = false; + ImGui::Separator(); + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, m_labels_map["Groove"] + ": "); + render_groove_float_input(m_labels_map["Depth"], m_groove.depth, m_groove.depth_init, m_groove.depth_tolerance); + render_groove_float_input(m_labels_map["Width"], m_groove.width, m_groove.width_init, m_groove.width_tolerance); + render_groove_angle_input(m_labels_map["Flap Angle"], m_groove.flaps_angle, m_groove.flaps_angle_init, 30.f, 120.f); + render_groove_angle_input(m_labels_map["Groove Angle"], m_groove.angle, m_groove.angle_init, 0.f, 15.f); + } + + ImGui::Separator(); + + // render "After Cut" section + + ImVec2 label_size; + for (const auto& item : m_part_orientation_names) { + const ImVec2 text_size = m_imgui->calc_text_size(item.second); + if (label_size.x < text_size.x) + label_size.x = text_size.x; + if (label_size.y < text_size.y) + label_size.y = text_size.y; + } + const float h_shift = label_size.x + m_imgui->scaled(3.f); + const float marker_size = label_size.y; + + auto render_part_name = [this, marker_size, has_connectors](const wxString& name, bool& keep_part, const ImU32& color) { + bool keep = true; + add_horizontal_shift(m_imgui->scaled(1.2f)); + m_imgui->checkbox((m_keep_as_parts ? _L("Part") : _L("Object")) + " " + name, has_connectors ? keep : keep_part); + render_color_marker(marker_size, color); + }; + + auto render_part_actions = [this, h_shift] (const wxString& suffix, const bool& keep_part, bool& place_on_cut_part, bool& rotate_part) + { + float shift = m_imgui->scaled(1.2f); + if (suffix == "##lower") + shift += h_shift; + m_imgui->disabled_begin(!keep_part || m_keep_as_parts); + add_horizontal_shift(shift); + if (m_imgui->radio_button(m_part_orientation_names.at("none") + suffix, !place_on_cut_part && !rotate_part)) { + rotate_part = false; + place_on_cut_part = false; + } + + add_horizontal_shift(shift); + if (m_imgui->radio_button(m_part_orientation_names.at("on_cut") + suffix, place_on_cut_part)) { + place_on_cut_part = !place_on_cut_part; + rotate_part = false; + } + + add_horizontal_shift(shift); + if (m_imgui->radio_button(m_part_orientation_names.at("flip") + suffix, rotate_part)) { + rotate_part = !rotate_part; + place_on_cut_part = false; + } + m_imgui->disabled_end(); + }; + + m_imgui->text(_L("Cut result") + ": "); + add_vertical_scaled_interval(0.5f); + + m_imgui->disabled_begin(has_connectors || m_keep_as_parts); + render_part_name("A", m_keep_upper, m_imgui->to_ImU32(UPPER_PART_COLOR)); + ImGui::SameLine(h_shift + ImGui::GetCurrentWindow()->WindowPadding.x); + render_part_name("B", m_keep_lower, m_imgui->to_ImU32(LOWER_PART_COLOR)); + m_imgui->disabled_end(); + + add_vertical_scaled_interval(0.5f); + + const ImVec2 pos = ImGui::GetCurrentWindow()->DC.CursorPos; + render_part_actions("##upper", m_keep_upper, m_place_on_cut_upper, m_rotate_upper); + + ImGui::GetCurrentWindow()->DC.CursorPos = pos; + render_part_actions("##lower", m_keep_lower, m_place_on_cut_lower, m_rotate_lower); + + add_vertical_scaled_interval(0.75f); + + m_imgui->disabled_begin(has_connectors || m_part_selection.valid() || mode == CutMode::cutTongueAndGroove); + m_imgui->text(_L("Cut into") + ":"); + + if (m_part_selection.valid()) + m_keep_as_parts = false; + + add_horizontal_scaled_interval(1.2f); + // TRN CutGizmo: RadioButton Cut into ... + if (m_imgui->radio_button(_L("Objects"), !m_keep_as_parts)) + m_keep_as_parts = false; + ImGui::SameLine(); + // TRN CutGizmo: RadioButton Cut into ... + if (m_imgui->radio_button(_L("Parts"), m_keep_as_parts)) + m_keep_as_parts = true; + + if (m_keep_as_parts) { + m_keep_upper = m_keep_lower = true; + m_place_on_cut_upper = m_place_on_cut_lower = false; + m_rotate_upper = m_rotate_lower = false; + } + m_imgui->disabled_end(); + } + + 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_angle < 0.f || m_connector_angle > float(PI) ) + m_connector_angle = 0.f; + + if (m_connector_type == CutConnectorType::Undef) + m_connector_type = CutConnectorType::Plug; + if (m_connector_style == int(CutConnectorStyle::Undef)) + m_connector_style = int(CutConnectorStyle::Prism); + if (m_connector_shape_id == int(CutConnectorShape::Undef)) + m_connector_shape_id = int(CutConnectorShape::Circle); +} + +void GLGizmoCut3D::init_input_window_data(CutConnectors &connectors) +{ + m_imperial_units = wxGetApp().app_config->get_bool("use_inches"); + 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 }; + float angle { 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; + angle = connector.z_angle; + 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 (!is_approx(angle, connector.z_angle)) + angle = 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 = 2.f * radius_tolerance; + m_connector_type = type; + m_connector_angle = angle; + m_connector_style = int(style); + m_connector_shape_id = int(shape); + } + + if (m_label_width == 0.f) { + for (const auto& item : m_labels_map) { + const float width = m_imgui->calc_text_size(item.second).x; + if (m_label_width < width) + m_label_width = width; + } + m_label_width += m_imgui->scaled(1.f); + } +} + +void GLGizmoCut3D::render_input_window_warning() const +{ + if (! m_invalid_connectors_idxs.empty()) { + wxString out = /*wxString(ImGui::WarningMarkerSmall)*/ _L("Warning") + ": " + _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("Warning") + ": " + _L("Select at least one object to keep after cutting.")); + if (!has_valid_contour()) + m_imgui->text(/*wxString(ImGui::WarningMarkerSmall)*/ _L("Warning") + ": " + _L("Cut plane is placed out of object")); + else if (!has_valid_groove()) + m_imgui->text(/*wxString(ImGui::WarningMarkerSmall)*/ _L("Warning") + ": " + _L("Cut plane with groove is invalid")); +} + +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(); + + if (!m_connectors_editing) // connectors mode + render_debug_input_window(x); +} + +bool GLGizmoCut3D::is_outside_of_cut_contour(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) == -1) { + m_info_stats.outside_cut_contour++; + return true; + } + + // check if connector bottom contour is out of clipping plane + const CutConnector& cur_connector = connectors[idx]; + const CutConnectorShape shape = CutConnectorShape(cur_connector.attribs.shape); + const int sectorCount = shape == CutConnectorShape::Triangle ? 3 : + shape == CutConnectorShape::Square ? 4 : + shape == CutConnectorShape::Circle ? 60: // supposably, 60 points are enough for conflict detection + shape == CutConnectorShape::Hexagon ? 6 : 1 ; + + indexed_triangle_set mesh; + auto& vertices = mesh.vertices; + vertices.reserve(sectorCount + 1); + + float fa = 2 * PI / sectorCount; + auto vec = Eigen::Vector2f(0, cur_connector.radius); + for (float angle = 0; angle < 2.f * PI; angle += fa) { + Vec2f p = Eigen::Rotation2Df(angle) * vec; + vertices.emplace_back(Vec3f(p(0), p(1), 0.f)); + } + its_transform(mesh, translation_transform(cur_pos) * m_rotation_m); + + for (const Vec3f& vertex : vertices) { + if (m_c->object_clipper()) { + int contour_idx = m_c->object_clipper()->is_projection_inside_cut(vertex.cast()); + bool is_invalid = (contour_idx == -1); + if (m_part_selection.valid() && ! is_invalid) { + assert(contour_idx >= 0); + const std::vector& ignored = *(m_part_selection.get_ignored_contours_ptr()); + is_invalid = (std::find(ignored.begin(), ignored.end(), size_t(contour_idx)) != ignored.end()); + } + if (is_invalid) { + m_info_stats.outside_cut_contour++; + return true; + } + } + } + + return false; +} + +bool GLGizmoCut3D::is_conflict_for_connector(size_t idx, const CutConnectors& connectors, const Vec3d cur_pos) +{ + if (is_outside_of_cut_contour(idx, connectors, cur_pos)) + 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); + + // check if connector's bounding box is inside the object's bounding box + if (!m_bounding_box.contains(cur_tbb)) { + m_info_stats.outside_bb++; + return true; + } + + // check if connectors are overlapping + 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::check_and_update_connectors_state() +{ + m_info_stats.invalidate(); + m_invalid_connectors_idxs.clear(); + if (CutMode(m_mode) != CutMode::cutPlanar) + 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; + 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()); + + for (size_t i = 0; i < connectors.size(); ++i) { + const CutConnector& connector = connectors[i]; + Vec3d pos = connector.pos + instance_offset + sla_shift * Vec3d::UnitZ(); // recalculate connector position to world position + if (is_conflict_for_connector(i, connectors, pos)) + m_invalid_connectors_idxs.emplace_back(i); + } +} + +void GLGizmoCut3D::toggle_model_objects_visibility() +{ + bool has_active_volume = false; + std::vector>* raycasters = m_parent.get_raycasters_for_picking(SceneRaycaster::EType::Volume); + for (const std::shared_ptr &raycaster : *raycasters) + if (raycaster->is_active()) { + has_active_volume = true; + break; + } + + if (m_part_selection.valid() && has_active_volume) + m_parent.toggle_model_objects_visibility(false); + else if (!m_part_selection.valid() && !has_active_volume) { + const Selection& selection = m_parent.get_selection(); + const ModelObjectPtrs& model_objects = selection.get_model()->objects; + m_parent.toggle_model_objects_visibility(true, model_objects[selection.get_object_idx()], selection.get_instance_idx()); + } +} + +void GLGizmoCut3D::render_connectors() +{ + ::glEnable(GL_DEPTH_TEST); + + if (cut_line_processing() || + CutMode(m_mode) != CutMode::cutPlanar || + 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 bool looking_forward = is_looking_forward(); + + 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. + assert(std::is_sorted(m_invalid_connectors_idxs.begin(), m_invalid_connectors_idxs.end())); + const bool conflict_connector = std::binary_search(m_invalid_connectors_idxs.begin(), m_invalid_connectors_idxs.end(), i); + if (conflict_connector) + render_color = CONNECTOR_ERR_COLOR; + else // default connector color + render_color = connector.attribs.type == CutConnectorType::Dowel ? DOWEL_COLOR : PLAG_COLOR; + + if (!m_connectors_editing) + render_color = CONNECTOR_ERR_COLOR; + else if (size_t(m_hover_id - m_connectors_group_id) == i) + render_color = conflict_connector ? HOVERED_ERR_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; + + const Camera& camera = wxGetApp().plater()->get_camera(); + if (connector.attribs.type == CutConnectorType::Dowel && + connector.attribs.style == CutConnectorStyle::Prism) { + if (m_connectors_editing) { + height = 0.05f; + if (!looking_forward) + pos += 0.05 * m_clp_normal; + } + else { + if (looking_forward) + pos -= static_cast(height) * m_clp_normal; + else + pos += static_cast(height) * m_clp_normal; + height *= 2; + } + } + else if (!looking_forward) + pos += 0.05 * m_clp_normal; + + const Transform3d view_model_matrix = camera.get_view_matrix() * translation_transform(pos) * m_rotation_m * + rotation_transform(-connector.z_angle * Vec3d::UnitZ()) * + 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_invalid_connectors_idxs.empty() || (!m_keep_upper && !m_keep_lower) || m_connectors_editing) + return false; + + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) + return has_valid_groove(); + + if (m_part_selection.valid()) + return ! m_part_selection.is_one_object(); + + return true; +} + +bool GLGizmoCut3D::has_valid_groove() const +{ + if (CutMode(m_mode) != CutMode::cutTongueAndGroove) + return true; + + const float flaps_width = -2.f * m_groove.depth / tan(m_groove.flaps_angle); + if (flaps_width > m_groove.width) + return false; + + const Selection& selection = m_parent.get_selection(); + const auto&list = selection.get_volume_idxs(); + // is more volumes selected? + if (list.empty()) + return false; + + const Transform3d cp_matrix = translation_transform(m_plane_center) * m_rotation_m; + + for (size_t id = 0; id < m_groove_vertices.size(); id += 2) { + const Vec3d beg = cp_matrix * m_groove_vertices[id]; + const Vec3d end = cp_matrix * m_groove_vertices[id + 1]; + + bool intersection = false; + for (const unsigned int volume_idx : list) { + const GLVolume* glvol = selection.get_volume(volume_idx); + if (!glvol->is_modifier && + glvol->mesh_raycaster->intersects_line(beg, end - beg, glvol->world_matrix())) { + intersection = true; + break; + } + } + if (!intersection) + return false; + } + + return true; +} + +bool GLGizmoCut3D::has_valid_contour() const +{ + const auto clipper = m_c->object_clipper(); + return clipper && clipper->has_valid_contour(); +} + +void GLGizmoCut3D::apply_connectors_in_model(ModelObject* mo, int &dowels_count) +{ + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) + return; + 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::Prism) + connector.height *= 2; + dowels_count ++; + } + else { + // calculate shift of the connector center regarding to the position on the cut plane + connector.pos += m_cut_normal * 0.5 * double(connector.height); + } + } + apply_cut_connectors(mo, _u8L("Connector")); + } +} + +Transform3d GLGizmoCut3D::get_cut_matrix(const Selection& selection) +{ + const int instance_idx = selection.get_instance_idx(); + const int object_idx = selection.get_object_idx(); + ModelObject* mo = selection.get_model()->objects[object_idx]; + if (!mo) + return Transform3d::Identity(); + + // 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; + + return translation_transform(cut_center_offset) * m_rotation_m; +} + +void update_object_cut_id(CutObjectBase& cut_id, ModelObjectCutAttributes attributes, const int dowels_count) +{ + // 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) || + attributes.has(ModelObjectCutAttribute::InvalidateCutInfo)) + return; + + if (cut_id.id().invalid()) + cut_id.init(); + // increase check sum, if it's needed + { + 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+= dowels_count; + if (cut_obj_cnt > 0) + cut_id.increase_check_sum(size_t(cut_obj_cnt)); + } +} + +void synchronize_model_after_cut(Model& model, const CutObjectBase& cut_id) +{ + for (ModelObject* obj : model.objects) + if (obj->is_cut() && obj->cut_id.has_same_id(cut_id) && !obj->cut_id.is_equal(cut_id)) + obj->cut_id.copy(cut_id); +} + +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(); + + // perform cut + { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Cut by Plane")); + + // This shall delete the part selection class and deallocate the memory. + ScopeGuard part_selection_killer([this]() { m_part_selection = PartSelection(); }); + + const bool cut_with_groove = CutMode(m_mode) == CutMode::cutTongueAndGroove; + const bool cut_by_contour = !cut_with_groove && m_part_selection.valid(); + + ModelObject* cut_mo = cut_by_contour ? m_part_selection.model_object() : nullptr; + if (cut_mo) + cut_mo->cut_connectors = mo->cut_connectors; + else + cut_mo = mo; + + int dowels_count = 0; + const bool has_connectors = !mo->cut_connectors.empty(); + // update connectors pos as offset of its center before cut performing + apply_connectors_in_model(cut_mo , dowels_count); + + wxBusyCursor wait; + + ModelObjectCutAttributes attributes = only_if(has_connectors ? true : m_keep_upper, ModelObjectCutAttribute::KeepUpper) | + only_if(has_connectors ? true : m_keep_lower, ModelObjectCutAttribute::KeepLower) | + only_if(has_connectors ? false : m_keep_as_parts, ModelObjectCutAttribute::KeepAsParts) | + 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(dowels_count > 0, ModelObjectCutAttribute::CreateDowels) | + only_if(!has_connectors && !cut_with_groove && cut_mo->cut_id.id().invalid(), ModelObjectCutAttribute::InvalidateCutInfo); + + // update cut_id for the cut object in respect to the attributes + update_object_cut_id(cut_mo->cut_id, attributes, dowels_count); + + Cut cut(cut_mo, instance_idx, get_cut_matrix(selection), attributes); + const ModelObjectPtrs& new_objects = cut_by_contour ? cut.perform_by_contour(m_part_selection.get_cut_parts(), dowels_count): + cut_with_groove ? cut.perform_with_groove(m_groove, m_rotation_m) : + cut.perform_with_plane(); + // save cut_id to post update synchronization + const CutObjectBase cut_id = cut_mo->cut_id; + + // update cut results on plater and in the model + plater->apply_cut_object_to_model(object_idx, new_objects); + + synchronize_model_after_cut(plater->model(), cut_id); + } +} + +// 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, Vec3d& pos, Vec3d& pos_world, bool respect_contours/* = true*/) +{ + 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 Camera& camera = wxGetApp().plater()->get_camera(); + + // Calculate intersection with the clipping plane. + const ClippingPlane* cp = m_c->object_clipper()->get_clipping_plane(true); + Vec3d point; + Vec3d direction; + Vec3d hit; + MeshRaycaster::line_from_mouse_pos(mouse_position, Transform3d::Identity(), camera, point, direction); + Vec3d normal = -cp->get_normal().cast(); + double den = normal.dot(direction); + if (den != 0.) { + double t = (-cp->get_offset() - normal.dot(point))/den; + hit = (point + t * direction); + } else + return false; + + // Now check if the hit is not obscured by a selected part on this side of the plane. + // FIXME: This would be better solved by remembering which contours are active. We will + // probably need that anyway because there is not other way to find out which contours + // to render. If you want to uncomment it, fix it first. It does not work yet. + /*for (size_t id = 0; id < m_part_selection.parts.size(); ++id) { + if (! m_part_selection.parts[id].selected) { + Vec3f pos, normal; + const ModelObject* model_object = m_part_selection.model_object; + const Vec3d volume_offset = m_part_selection.model_object->volumes[id]->get_offset(); + Transform3d tr = model_object->instances[m_part_selection.instance_idx]->get_matrix() * model_object->volumes[id]->get_matrix(); + if (m_part_selection.parts[id].raycaster.unproject_on_mesh(mouse_position, tr, camera, pos, normal)) + return false; + } + }*/ + + if (respect_contours) + { + // Do not react to clicks outside a contour (or inside a contour that is ignored) + int cont_id = m_c->object_clipper()->is_projection_inside_cut(hit); + if (cont_id == -1) + return false; + if (m_part_selection.valid()) { + const std::vector& ign = *m_part_selection.get_ignored_contours_ptr(); + if (std::find(ign.begin(), ign.end(), cont_id) != ign.end()) + return false; + } + } + + + // recalculate hit to object's local position + Vec3d hit_d = hit; + hit_d -= mi->get_offset(); + hit_d[Z] -= sla_shift; + + // Return both the point and the facet normal. + pos = hit_d; + pos_world = hit; + + return true; +} + +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(); + check_and_update_connectors_state(); +} + +void GLGizmoCut3D::init_connector_shapes() +{ + for (const CutConnectorType& type : {CutConnectorType::Dowel, CutConnectorType::Plug, CutConnectorType::Snap}) + for (const CutConnectorStyle& style : {CutConnectorStyle::Frustum, CutConnectorStyle::Prism}) { + if (type == CutConnectorType::Dowel && style == CutConnectorStyle::Frustum) + continue; + for (const CutConnectorShape& shape : {CutConnectorShape::Circle, CutConnectorShape::Hexagon, CutConnectorShape::Square, CutConnectorShape::Triangle}) { + if (type == CutConnectorType::Snap && shape != CutConnectorShape::Circle) + continue; + const CutConnectorAttributes attribs = { type, style, shape }; + indexed_triangle_set its = 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) }; + + if (m_connector_type == CutConnectorType::Snap) { + indexed_triangle_set its = get_connector_mesh(attribs); + m_shapes[attribs].reset(); + m_shapes[attribs].model.init_from(its); + m_shapes[attribs].mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); + + //const indexed_triangle_set its = get_connector_mesh(attribs); + //m_connector_mesh.clear(); + //m_connector_mesh = TriangleMesh(its); + } + + +} + +bool GLGizmoCut3D::cut_line_processing() const +{ + return !m_line_beg.isApprox(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()) { + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) + m_groove_editing = true; + reset_cut_by_contours(); + + 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; + + 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(); + + const Vec3d new_plane_center = m_bb_center + cross_dir * cross_dir.dot(pt - m_bb_center); + // update transformed bb + const auto new_tbb = transformed_bounding_box(new_plane_center, m); + const GLVolume* first_volume = m_parent.get_selection().get_first_volume(); + Vec3d instance_offset = first_volume->get_instance_offset(); + instance_offset[Z] += first_volume->get_sla_shift_z(); + + const Vec3d trans_center_pos = m.inverse() * (new_plane_center - instance_offset) + new_tbb.center(); + if (new_tbb.contains(trans_center_pos)) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Cut by line"), UndoRedo::SnapshotType::GizmoAction); + m_transformed_bounding_box = new_tbb; + set_center(new_plane_center); + m_start_dragging_m = m_rotation_m = m; + m_ar_plane_center = m_plane_center; + } + + m_angle_arc.reset(); + discard_cut_line_processing(); + + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { + m_groove_editing = false; + reset_cut_by_contours(); + } + } + 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; + + Vec3d pos; + Vec3d pos_world; + if (unproject_on_cut_plane(mouse_position.cast(), pos, pos_world)) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Add connector"), UndoRedo::SnapshotType::GizmoAction); + unselect_all_connectors(); + + connectors.emplace_back(pos, m_rotation_m, + m_connector_size * 0.5f, m_connector_depth_ratio, + m_connector_size_tolerance * 0.5f, m_connector_depth_ratio_tolerance, + m_connector_angle, + 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(); + check_and_update_connectors_state(); + + return true; + } + return false; +} + +bool GLGizmoCut3D::delete_selected_connectors(CutConnectors& connectors) +{ + if (connectors.empty()) + return false; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("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(); + check_and_update_connectors_state(); + return true; +} + +void GLGizmoCut3D::select_connector(int idx, bool select) +{ + m_selected[idx] = select; + if (select) + ++m_selected_count; + else + --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) + return false; + + if ( (m_hover_id < 0 || m_hover_id == CutPlane) && shift_down && ! m_connectors_editing && + (action == SLAGizmoEventType::LeftDown || action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::Moving) ) + return process_cut_line(action, mouse_position); + + if (!m_keep_upper || !m_keep_lower) + return false; + + 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)); +} + +void GLGizmoCut3D::data_changed(bool is_serializing) +{ + update_bb(); + if (auto oc = m_c->object_clipper()) + oc->set_behavior(m_connectors_editing, m_connectors_editing, double(m_contour_width)); +} + + + + +indexed_triangle_set GLGizmoCut3D::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.type == CutConnectorType::Snap) + connector_mesh = its_make_snap(1.0, 1.0, m_snap_space_proportion, m_snap_bulge_proportion); + else if (connector_attributes.style == CutConnectorStyle::Prism) + connector_mesh = its_make_cylinder(1.0, 1.0, (2 * PI / sectorCount)); + else if (connector_attributes.type == CutConnectorType::Plug) + connector_mesh = its_make_frustum(1.0, 1.0, (2 * PI / sectorCount)); + else + connector_mesh = its_make_frustum_dowel(1.0, 1.0, sectorCount); + + return connector_mesh; +} + +void GLGizmoCut3D::apply_cut_connectors(ModelObject* mo, const std::string& connector_name) +{ + if (mo->cut_connectors.empty()) + return; + + using namespace Geometry; + + size_t connector_id = mo->cut_id.connectors_cnt(); + for (const CutConnector& connector : mo->cut_connectors) { + TriangleMesh mesh = TriangleMesh(get_connector_mesh(connector.attribs)); + // Mesh will be centered when loading. + ModelVolume* new_volume = mo->add_volume(std::move(mesh), ModelVolumeType::NEGATIVE_VOLUME); + + // Transform the new modifier to be aligned inside the instance + new_volume->set_transformation(translation_transform(connector.pos) * connector.rotation_m * + rotation_transform(-connector.z_angle * Vec3d::UnitZ()) * + 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 = connector_name + "-" + std::to_string(++connector_id); + } + mo->cut_id.increase_connectors_cnt(mo->cut_connectors.size()); + + // delete all connectors + mo->cut_connectors.clear(); +} + + + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp index cf7c7ac042..89c1b92833 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp @@ -1,72 +1,391 @@ +///|/ Copyright (c) Prusa Research 2019 - 2023 Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Enrico Turri @enricoturri1966, Filip Sykala @Jony01, Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef slic3r_GLGizmoCut_hpp_ #define slic3r_GLGizmoCut_hpp_ #include "GLGizmoBase.hpp" +#include "slic3r/GUI/GLSelectionRectangle.hpp" #include "slic3r/GUI/GLModel.hpp" +#include "slic3r/GUI/I18N.hpp" #include "libslic3r/TriangleMesh.hpp" -#include "libslic3r/ObjectID.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/CutUtils.hpp" +#include "imgui/imgui.h" namespace Slic3r { + +enum class CutConnectorType : int; +class ModelVolume; +class GLShaderProgram; +struct CutConnectorAttributes; + namespace GUI { +class Selection; -class GLGizmoCut : public GLGizmoBase +enum class SLAGizmoEventType : unsigned char; + +namespace CommonGizmosDataObjects { class ObjectClipper; } + +class GLGizmoCut3D : public GLGizmoBase { - static const double Offset; - static const double Margin; - - 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 }; - // BBS: m_do_segment - bool m_cut_to_parts {false}; - bool m_do_segment{ false }; - double m_segment_smoothing_alpha{ 0.5 }; - int m_segment_number{ 5 }; - - struct CutContours - { - TriangleMesh mesh; - GLModel contours; - double cut_z{ 0.0 }; - Vec3d position{ Vec3d::Zero() }; - Vec3d shift{ Vec3d::Zero() }; - ObjectID object_id; - int instance_idx{ -1 }; + enum GrabberID { + X = 0, + Y, + Z, + CutPlane, + CutPlaneZRotation, + CutPlaneXMove, + CutPlaneYMove, + Count, }; - CutContours m_cut_contours; + 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() }; + Transform3d m_start_dragging_m{ Transform3d::Identity() }; + + 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() }; + + BoundingBoxf3 m_bounding_box; + BoundingBoxf3 m_transformed_bounding_box; + + // values from RotationGizmo + double m_radius{ 0.0 }; + double m_grabber_radius{ 0.0 }; + double m_grabber_connection_len{ 0.0 }; + Vec3d m_cut_plane_start_move_pos {Vec3d::Zero()}; + + 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 + 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() }; + + GLModel m_grabber_connection; + GLModel m_cut_line; + + PickingModel m_plane; + PickingModel m_sphere; + PickingModel m_cone; + PickingModel m_cube; + 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; + Vec3d m_cut_normal; + + struct InvalidConnectorsStatistics + { + 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_keep_as_parts{ false }; + bool m_place_on_cut_upper{ true }; + bool m_place_on_cut_lower{ false }; + bool m_rotate_upper{ false }; + bool m_rotate_lower{ false }; + + // Input params for cut with tongue and groove + Cut::Groove m_groove; + bool m_groove_editing { false }; + + bool m_is_slider_editing_done { false }; + + // Input params for cut with snaps + float m_snap_bulge_proportion{ 0.15f }; + float m_snap_space_proportion{ 0.3f }; + + 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_angle{ 0.f }; + + float m_connector_depth_ratio_tolerance{ 0.1f }; + float m_connector_size_tolerance{ 0.f }; + + float m_label_width{ 0.f }; + float m_control_width{ 200.f }; + bool m_imperial_units{ false }; + + float m_contour_width{ 0.4f }; + float m_cut_plane_radius_koef{ 1.5f }; + float m_shortcut_label_width{ -1.f }; + + mutable std::vector m_selected; // which pins are currently selected + int m_selected_count{ 0 }; + + GLSelectionRectangle m_selection_rectangle; + + std::vector m_invalid_connectors_idxs; + bool m_was_cut_plane_dragged { false }; + bool m_was_contour_selected { false }; + + // Vertices of the groove used to detection if groove is valid + std::vector m_groove_vertices; + + class PartSelection { + public: + PartSelection() = default; + PartSelection(const ModelObject* mo, const Transform3d& cut_matrix, int instance_idx, const Vec3d& center, const Vec3d& normal, const CommonGizmosDataObjects::ObjectClipper& oc); + PartSelection(const ModelObject* mo, int instance_idx_in); + ~PartSelection() { m_model.clear_objects(); } + + struct Part { + GLModel glmodel; + MeshRaycaster raycaster; + bool selected; + bool is_modifier; + }; + + void render(const Vec3d* normal, GLModel& sphere_model); + void toggle_selection(const Vec2d& mouse_pos); + void turn_over_selection(); + ModelObject* model_object() { return m_model.objects.front(); } + bool valid() const { return m_valid; } + bool is_one_object() const; + const std::vector& parts() const { return m_parts; } + const std::vector* get_ignored_contours_ptr() const { return (valid() ? &m_ignored_contours : nullptr); } + + std::vector get_cut_parts(); + + private: + Model m_model; + int m_instance_idx; + std::vector m_parts; + bool m_valid = false; + std::vector, std::vector>> m_contour_to_parts; // for each contour, there is a vector of parts above and a vector of parts below + std::vector m_ignored_contours; // contour that should not be rendered (the parts on both sides will both be parts of the same object) + + std::vector m_contour_points; // Debugging + std::vector> m_debug_pts; // Debugging + + void add_object(const ModelObject* object); + }; + + PartSelection m_part_selection; + + bool m_show_shortcuts{ false }; + std::vector> m_shortcuts; + + enum class CutMode { + cutPlanar + , cutTongueAndGroove + //, cutGrig + //,cutRadial + //,cutModular + }; + + 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; + int m_connector_style; + + std::vector m_connector_shapes; + int m_connector_shape_id; + + std::vector m_axis_names; + + std::map m_part_orientation_names; + + std::map m_labels_map; 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, Vec3d& pos, Vec3d& pos_world, bool respect_contours = true); + 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(); } + bool is_looking_forward() const; + + /// + /// Drag of plane + /// + /// Keep information about mouse click + /// Return True when use the information otherwise False. + bool on_mouse(const wxMouseEvent &mouse_event) override; + + void shift_cut(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 invalidate_cut_plane(); + + BoundingBoxf3 bounding_box() const; + BoundingBoxf3 transformed_bounding_box(const Vec3d& plane_center, const Transform3d& rotation_m = Transform3d::Identity()) 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_update(const UpdateData& data) override; - virtual void on_render() override; - virtual void on_render_for_picking() override; - virtual void on_render_input_window(float x, float y, float bottom_limit) override; + 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(GrabberID axis, const Linef3&mouse_ray) const; + void dragging_grabber_move(const GLGizmoBase::UpdateData &data); + void dragging_grabber_rotation(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(float x); + 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 flip_cut_plane(); + void process_contours(); + void reset_cut_by_contours(); + void render_flip_plane_button(bool disable_pred = false); + void add_vertical_scaled_interval(float interval); + void add_horizontal_scaled_interval(float interval); + void add_horizontal_shift(float shift); + void render_color_marker(float size, const ImU32& color); + void render_groove_float_input(const std::string &label, float &in_val, const float &init_val, float &in_tolerance); + void render_groove_angle_input(const std::string &label, float &in_val, const float &init_val, float min_val, float max_val); + bool render_angle_input(const std::string& label, float& in_val, const float& init_val, float min_val, float max_val); + void render_snap_specific_input(const std::string& label, const wxString& tooltip, float& in_val, const float& init_val, const float min_val, const float max_val); + 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; + void update_raycasters_for_picking(); + void set_volumes_picking_state(bool state); + void update_raycasters_for_picking_transform(); + + void update_plane_model(); + + 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() const override { return _u8L("Cut gizmo editing"); } + + void data_changed(bool is_serializing) override; + Transform3d get_cut_matrix(const Selection& selection); 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¢er, bool update_tbb = false); + void switch_to_mode(size_t new_mode); + bool render_cut_mode_combo(); + bool render_combo(const std::string&label, const std::vector&lines, int&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, float min_val = -0.1f, float max_tolerance = -0.1f); + 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); + bool is_outside_of_cut_contour(size_t idx, const CutConnectors& connectors, const Vec3d cur_pos); + bool is_conflict_for_connector(size_t idx, const CutConnectors& connectors, const Vec3d cur_pos); + void render_connectors(); + + bool can_perform_cut() const; + bool has_valid_groove() const; + bool has_valid_contour() const; + void apply_connectors_in_model(ModelObject* mo, int &dowels_count); + bool cut_line_processing() const; + void discard_cut_line_processing(); + + void apply_color_clip_plane_colors(); + void render_cut_plane(); + static 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(GrabberID axis, const ColorRGBA& color); + void render_grabber_connection(const ColorRGBA& color, Transform3d view_matrix, double line_len_koef = 1.0); + void render_cut_plane_grabbers(); + void render_cut_line(); + void perform_cut(const Selection&selection); + void set_center_pos(const Vec3d¢er_pos, bool update_tbb = false); + void 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); + void check_and_update_connectors_state(); + + void toggle_model_objects_visibility(); + + indexed_triangle_set its_make_groove_plane(); + + indexed_triangle_set get_connector_mesh(CutConnectorAttributes connector_attributes); + void apply_cut_connectors(ModelObject* mo, const std::string& connector_name); }; } // namespace GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 5e113372e6..3398362855 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -20,8 +20,7 @@ #include "slic3r/GUI/Gizmos/GLGizmoFlatten.hpp" //#include "slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp" #include "slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp" -// BBS -#include "slic3r/GUI/Gizmos/GLGizmoAdvancedCut.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoCut.hpp" //#include "slic3r/GUI/Gizmos/GLGizmoFaceDetector.hpp" //#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp" @@ -193,7 +192,7 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoRotate3D(m_parent, m_is_dark ? "toolbar_rotate_dark.svg" : "toolbar_rotate.svg", EType::Rotate, &m_object_manipulation)); m_gizmos.emplace_back(new GLGizmoScale3D(m_parent, m_is_dark ? "toolbar_scale_dark.svg" : "toolbar_scale.svg", EType::Scale, &m_object_manipulation)); m_gizmos.emplace_back(new GLGizmoFlatten(m_parent, m_is_dark ? "toolbar_flatten_dark.svg" : "toolbar_flatten.svg", EType::Flatten)); - m_gizmos.emplace_back(new GLGizmoAdvancedCut(m_parent, m_is_dark ? "toolbar_cut_dark.svg" : "toolbar_cut.svg", EType::Cut)); + m_gizmos.emplace_back(new GLGizmoCut3D(m_parent, m_is_dark ? "toolbar_cut_dark.svg" : "toolbar_cut.svg", EType::Cut)); m_gizmos.emplace_back(new GLGizmoMeshBoolean(m_parent, m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg", EType::MeshBoolean)); m_gizmos.emplace_back(new GLGizmoFdmSupports(m_parent, m_is_dark ? "toolbar_support_dark.svg" : "toolbar_support.svg", EType::FdmSupports)); m_gizmos.emplace_back(new GLGizmoSeam(m_parent, m_is_dark ? "toolbar_seam_dark.svg" : "toolbar_seam.svg", EType::Seam)); @@ -446,7 +445,7 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p else if (m_current == Measure) return dynamic_cast(m_gizmos[Measure].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); + return dynamic_cast(m_gizmos[Cut].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == MeshBoolean) return dynamic_cast(m_gizmos[MeshBoolean].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else @@ -861,13 +860,10 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) // m_parent.set_cursor(GLCanvas3D::Cross); processed = true; } - else*/ if (m_current == Cut) - { - // BBS -#if 0 + else*/ if (m_current == Cut) { auto do_move = [this, &processed](double delta_z) { - GLGizmoAdvancedCut* cut = dynamic_cast(get_current()); - cut->set_cut_z(delta_z + cut->get_cut_z()); + GLGizmoCut3D* cut = dynamic_cast(get_current()); + cut->shift_cut(delta_z); processed = true; }; @@ -875,10 +871,13 @@ 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; } } -#endif - } else if (m_current == Simplify && keyCode == WXK_ESCAPE) { + } + else if (m_current == Simplify && keyCode == WXK_ESCAPE) { GLGizmoSimplify *simplify = dynamic_cast(get_current()); if (simplify != nullptr) processed = simplify->on_esc_key_down(); diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 47b9ea21be..56cff4b468 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -511,10 +511,12 @@ void ImGuiWrapper::render() m_new_frame_open = false; } -ImVec2 ImGuiWrapper::calc_text_size(const wxString &text, float wrap_width) const +ImVec2 ImGuiWrapper::calc_text_size(const wxString &text, + bool hide_text_after_double_hash, + float wrap_width) const { auto text_utf8 = into_u8(text); - ImVec2 size = ImGui::CalcTextSize(text_utf8.c_str(), NULL, false, wrap_width); + ImVec2 size = ImGui::CalcTextSize(text_utf8.c_str(), NULL, hide_text_after_double_hash, wrap_width); /*#ifdef __linux__ size.x *= m_style_scaling; @@ -773,16 +775,30 @@ void ImGuiWrapper::end() ImGui::End(); } -bool ImGuiWrapper::button(const wxString &label) +bool ImGuiWrapper::button(const wxString &label, const wxString& tooltip) { auto label_utf8 = into_u8(label); - return ImGui::Button(label_utf8.c_str()); + const bool ret = ImGui::Button(label_utf8.c_str()); + + if (!tooltip.IsEmpty() && ImGui::IsItemHovered()) { + auto tooltip_utf8 = into_u8(tooltip); + ImGui::SetTooltip(tooltip_utf8.c_str(), nullptr); + } + + return ret; } -bool ImGuiWrapper::bbl_button(const wxString &label) +bool ImGuiWrapper::bbl_button(const wxString &label, const wxString& tooltip) { auto label_utf8 = into_u8(label); - return ImGui::BBLButton(label_utf8.c_str()); + const bool ret = ImGui::BBLButton(label_utf8.c_str()); + + if (!tooltip.IsEmpty() && ImGui::IsItemHovered()) { + auto tooltip_utf8 = into_u8(tooltip); + ImGui::SetTooltip(tooltip_utf8.c_str(), nullptr); + } + + return ret; } bool ImGuiWrapper::button(const wxString& label, float width, float height) @@ -791,6 +807,17 @@ bool ImGuiWrapper::button(const wxString& label, float width, float height) return ImGui::Button(label_utf8.c_str(), ImVec2(width, height)); } +bool ImGuiWrapper::button(const wxString& label, const ImVec2 &size, bool enable) +{ + disabled_begin(!enable); + + auto label_utf8 = into_u8(label); + bool res = ImGui::Button(label_utf8.c_str(), size); + + disabled_end(); + return (enable) ? res : false; +} + bool ImGuiWrapper::radio_button(const wxString &label, bool active) { auto label_utf8 = into_u8(label); @@ -965,6 +992,8 @@ bool ImGuiWrapper::slider_float(const char* label, float* v, float v_min, float m_last_slider_status.edited = ImGui::IsItemEdited(); m_last_slider_status.clicked = ImGui::IsItemClicked(); m_last_slider_status.deactivated_after_edit = ImGui::IsItemDeactivatedAfterEdit(); + if (!m_last_slider_status.can_take_snapshot) + m_last_slider_status.can_take_snapshot = ImGui::IsItemClicked(); if (!tooltip.empty() && ImGui::IsItemHovered()) this->tooltip(into_u8(tooltip).c_str(), max_tooltip_width); @@ -1103,25 +1132,34 @@ bool ImGuiWrapper::image_button(const wchar_t icon, const wxString& tooltip) return res; } -bool ImGuiWrapper::combo(const wxString& label, const std::vector& options, int& selection) +bool ImGuiWrapper::combo(const wxString& label, const std::vector& options, int& selection, ImGuiComboFlags flags/* = 0*/, float label_width/* = 0.0f*/, float item_width/* = 0.0f*/) +{ + return combo(into_u8(label), options, selection, flags, label_width, item_width); +} + +bool ImGuiWrapper::combo(const std::string& label, const std::vector& options, int& selection, ImGuiComboFlags flags/* = 0*/, float label_width/* = 0.0f*/, float item_width/* = 0.0f*/) { // this is to force the label to the left of the widget: - text(label); - ImGui::SameLine(); + const bool hidden_label = boost::starts_with(label, "##"); + if (!label.empty() && !hidden_label) { + text(label); + ImGui::SameLine(label_width); + } + ImGui::PushItemWidth(item_width); int selection_out = selection; bool res = false; const char *selection_str = selection < int(options.size()) && selection >= 0 ? options[selection].c_str() : ""; - if (ImGui::BeginCombo("", selection_str)) { + if (ImGui::BeginCombo(hidden_label ? label.c_str() : ("##" + label).c_str(), selection_str, flags)) { for (int i = 0; i < (int)options.size(); i++) { if (ImGui::Selectable(options[i].c_str(), i == selection)) { selection_out = i; + res = true; } } ImGui::EndCombo(); - res = true; } selection = selection_out; diff --git a/src/slic3r/GUI/ImGuiWrapper.hpp b/src/slic3r/GUI/ImGuiWrapper.hpp index 38c9bac819..05468ab042 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -74,6 +74,11 @@ public: bool edited { false }; bool clicked { false }; bool deactivated_after_edit { false }; + // flag to indicate possibility to take snapshot from the slider value + // It's used from Gizmos to take snapshots just from the very beginning of the editing + bool can_take_snapshot { false }; + // When Undo/Redo snapshot is taken, then call this function + void invalidate_snapshot() { can_take_snapshot = false; } }; ImGuiWrapper(); @@ -95,12 +100,13 @@ public: float scaled(float x) const { return x * m_font_size; } ImVec2 scaled(float x, float y) const { return ImVec2(x * m_font_size, y * m_font_size); } - ImVec2 calc_text_size(const wxString &text, float wrap_width = -1.0f) const; + ImVec2 calc_text_size(const wxString &text, bool hide_text_after_double_hash = false, float wrap_width = -1.0f) const; ImVec2 calc_button_size(const wxString &text, const ImVec2 &button_size = ImVec2(0, 0)) const; ImVec2 get_item_spacing() const; float get_slider_float_height() const; const LastSliderStatus& get_last_slider_status() const { return m_last_slider_status; } + LastSliderStatus& get_last_slider_status() { return m_last_slider_status; } void set_next_window_pos(float x, float y, int flag, float pivot_x = 0.0f, float pivot_y = 0.0f); void set_next_window_bg_alpha(float alpha); @@ -118,9 +124,10 @@ public: bool begin(const wxString& name, bool* close, int flags = 0); void end(); - bool button(const wxString &label); - bool bbl_button(const wxString &label); - bool button(const wxString& label, float width, float height); + bool button(const wxString &label, const wxString& tooltip = {}); + bool bbl_button(const wxString &label, const wxString& tooltip = {}); + bool button(const wxString& label, float width, float height); + bool button(const wxString& label, const ImVec2 &size, bool enable); // default size = ImVec2(0.f, 0.f) bool radio_button(const wxString &label, bool active); bool input_double(const std::string &label, const double &value, const std::string &format = "%.3f"); bool input_double(const wxString &label, const double &value, const std::string &format = "%.3f"); @@ -157,7 +164,9 @@ public: bool image_button(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0.0, 0.0), const ImVec2& uv1 = ImVec2(1.0, 1.0), int frame_padding = -1, const ImVec4& bg_col = ImVec4(0.0, 0.0, 0.0, 0.0), const ImVec4& tint_col = ImVec4(1.0, 1.0, 1.0, 1.0), ImGuiButtonFlags flags = 0); bool image_button(const wchar_t icon, const wxString& tooltip = L""); - bool combo(const wxString& label, const std::vector& options, int& selection); // Use -1 to not mark any option as selected + // Use selection = -1 to not mark any option as selected + bool combo(const std::string& label, const std::vector& options, int& selection, ImGuiComboFlags flags = 0, float label_width = 0.0f, float item_width = 0.0f); + bool combo(const wxString& label, const std::vector& options, int& selection, ImGuiComboFlags flags = 0, float label_width = 0.0f, float item_width = 0.0f); bool undo_redo_list(const ImVec2& size, const bool is_undo, bool (*items_getter)(const bool, int, const char**), int& hovered, int& selected, int& mouse_wheel); void search_list(const ImVec2& size, bool (*items_getter)(int, const char** label, const char** tooltip), char* search_str, Search::OptionViewParameters &view_params, diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index 8a91b80b8f..64060b6958 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2018 - 2023 Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966, David Kocík @kocikdav, Lukáš Hejl @hejllukas, Vojtěch Král @vojtechkral +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "MsgDialog.hpp" #include @@ -411,6 +415,58 @@ InfoDialog::InfoDialog(wxWindow* parent, const wxString &title, const wxString& finalize(); } +wxString get_wraped_wxString(const wxString& in, size_t line_len /*=80*/) +{ + wxString out; + + for (size_t i = 0; i < in.size();) { + // Overwrite the character (space or newline) starting at ibreak? + bool overwrite = false; + // UTF8 representation of wxString. + // Where to break the line, index of character at the start of a UTF-8 sequence. + size_t ibreak = size_t(-1); + // Overwrite the character at ibreak (it is a whitespace) or not? + size_t j = i; + for (size_t cnt = 0; j < in.size();) { + if (bool newline = in[j] == '\n'; in[j] == ' ' || in[j] == '\t' || newline) { + // Overwrite the whitespace. + ibreak = j ++; + overwrite = true; + if (newline) + break; + } else if (in[j] == '/' +#ifdef _WIN32 + || in[j] == '\\' +#endif // _WIN32 + ) { + // Insert after the slash. + ibreak = ++ j; + overwrite = false; + } else + j += get_utf8_sequence_length(in.c_str() + j, in.size() - j); + if (++ cnt == line_len) { + if (ibreak == size_t(-1)) { + ibreak = j; + overwrite = false; + } + break; + } + } + if (j == in.size()) { + out.append(in.begin() + i, in.end()); + break; + } + assert(ibreak != size_t(-1)); + out.append(in.begin() + i, in.begin() + ibreak); + out.append('\n'); + i = ibreak; + if (overwrite) + ++ i; + } + + return out; +} + // InfoDialog DownloadDialog::DownloadDialog(wxWindow *parent, const wxString &msg, const wxString &title, bool is_marked_msg /* = false*/, long style /* = wxOK | wxICON_INFORMATION*/) : MsgDialog(parent, title, msg, style), msg(msg) diff --git a/src/slic3r/GUI/MsgDialog.hpp b/src/slic3r/GUI/MsgDialog.hpp index c9b0ded3c5..a136d2eed1 100644 --- a/src/slic3r/GUI/MsgDialog.hpp +++ b/src/slic3r/GUI/MsgDialog.hpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2018 - 2022 Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, David Kocík @kocikdav, Lukáš Hejl @hejllukas, Vojtěch Bubník @bubnikv, Vojtěch Král @vojtechkral +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef slic3r_MsgDialog_hpp_ #define slic3r_MsgDialog_hpp_ @@ -128,6 +132,8 @@ public: virtual ~WarningDialog() = default; }; +wxString get_wraped_wxString(const wxString& text_in, size_t line_len = 80); + #if 1 // Generic static line, used intead of wxStaticLine //class StaticLine: public wxTextCtrl diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 4b3aa5d170..9368550288 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1,3 +1,22 @@ +///|/ Copyright (c) Prusa Research 2018 - 2023 Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena, Oleksandra Iushchenko @YuSanka, Enrico Turri @enricoturri1966, Tomáš Mészáros @tamasmeszaros, David Kocík @kocikdav, Lukáš Hejl @hejllukas, Pavel Mikuš @Godrak, Filip Sykala @Jony01, Vojtěch Král @vojtechkral +///|/ Copyright (c) 2022 Michael Kirsch +///|/ Copyright (c) 2021 Boleslaw Ciesielski +///|/ Copyright (c) 2019 John Drake @foxox +///|/ +///|/ ported from lib/Slic3r/GUI/Plater.pm: +///|/ Copyright (c) Prusa Research 2016 - 2019 Vojtěch Bubník @bubnikv, Vojtěch Král @vojtechkral, Enrico Turri @enricoturri1966, Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Tomáš Mészáros @tamasmeszaros +///|/ Copyright (c) 2018 Martin Loidl @LoidlM +///|/ Copyright (c) 2017 Matthias Gazzari @qtux +///|/ Copyright (c) Slic3r 2012 - 2016 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2017 Joseph Lenox @lordofhyphens +///|/ Copyright (c) 2015 Daren Schwenke +///|/ Copyright (c) 2014 Mark Hindess +///|/ Copyright (c) 2012 Mike Sheldrake @mesheldrake +///|/ Copyright (c) 2012 Henrik Brix Andersen @henrikbrixandersen +///|/ Copyright (c) 2012 Sam Wong +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "Plater.hpp" #include "libslic3r/Config.hpp" @@ -128,6 +147,7 @@ #include "Gizmos/GLGizmosManager.hpp" #endif // __APPLE__ +#include #include // Needs to be last because reasons :-/ #include "WipeTowerDialog.hpp" @@ -8273,14 +8293,6 @@ void Plater::add_model(bool imperial_units, std::string fname) wxGetApp().mainframe->update_title(); } } -std::array get_cut_plane(const BoundingBoxf3& bbox, const double& cut_height) { - std::array plane_pts; - plane_pts[0] = Vec3d(bbox.min(0), bbox.min(1), cut_height); - plane_pts[1] = Vec3d(bbox.max(0), bbox.min(1), cut_height); - plane_pts[2] = Vec3d(bbox.max(0), bbox.max(1), cut_height); - plane_pts[3] = Vec3d(bbox.min(0), bbox.max(1), cut_height); - return plane_pts; -} void Plater::calib_pa(const Calib_Params& params) { @@ -8403,6 +8415,36 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) changed_objects({ 0 }); } +void Plater::cut_horizontal(size_t obj_idx, size_t instance_idx, double z, ModelObjectCutAttributes attributes) +{ + wxCHECK_RET(obj_idx < p->model.objects.size(), "obj_idx out of bounds"); + 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; + + wxBusyCursor wait; + const auto new_objects = Cut::cut_horizontal(object, instance_idx, z, attributes); + + remove(obj_idx); + p->load_model_objects(new_objects); + + // 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; + for (size_t i = 0; i < new_objects.size(); ++i) + selection.add_object((unsigned int)(last_id - i), i == 0); +} + void Plater::_calib_pa_tower(const Calib_Params& params) { add_model(false, Slic3r::resources_dir() + "/calib/pressure_advance/tower_with_seam.stl"); @@ -8439,8 +8481,7 @@ void Plater::_calib_pa_tower(const Calib_Params& params) { auto new_height = std::ceil((params.end - params.start) / params.step) + 1; auto obj_bb = model().objects[0]->bounding_box(); if (new_height < obj_bb.size().z()) { - std::array plane_pts = get_cut_plane(obj_bb, new_height); - cut(0, 0, plane_pts, ModelObjectCutAttribute::KeepLower); + cut_horizontal(0, 0, new_height, ModelObjectCutAttribute::KeepLower); } _calib_pa_select_added_objects(); @@ -8581,8 +8622,7 @@ void Plater::calib_temp(const Calib_Params& params) { // add EPSILON offset to avoid cutting at the exact location where the flat surface is auto new_height = block_count * 10.0 + EPSILON; if (new_height < obj_bb.size().z()) { - std::array plane_pts = get_cut_plane(obj_bb, new_height); - cut(0, 0, plane_pts, ModelObjectCutAttribute::KeepLower); + cut_horizontal(0, 0, new_height, ModelObjectCutAttribute::KeepLower); } } @@ -8592,8 +8632,7 @@ void Plater::calib_temp(const Calib_Params& params) { if(block_count > 0){ auto new_height = block_count * 10.0 + EPSILON; if (new_height < obj_bb.size().z()) { - std::array plane_pts = get_cut_plane(obj_bb, new_height); - cut(0, 0, plane_pts, ModelObjectCutAttribute::KeepUpper); + cut_horizontal(0, 0, new_height, ModelObjectCutAttribute::KeepUpper); } } @@ -8661,8 +8700,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) auto obj_bb = obj->bounding_box(); auto height = (params.end - params.start + 1) / params.step; if (height < obj_bb.size().z()) { - std::array plane_pts = get_cut_plane(obj_bb, height); - cut(0, 0, plane_pts, ModelObjectCutAttribute::KeepLower); + cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower); } auto new_params = params; @@ -8710,8 +8748,7 @@ void Plater::calib_retraction(const Calib_Params& params) auto obj_bb = obj->bounding_box(); auto height = 1.0 + 0.4 + ((params.end - params.start)) / params.step; if (height < obj_bb.size().z()) { - std::array plane_pts = get_cut_plane(obj_bb, height); - cut(0, 0, plane_pts, ModelObjectCutAttribute::KeepLower); + cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower); } p->background_process.fff_print()->set_calib_params(params); @@ -8752,8 +8789,7 @@ void Plater::calib_VFA(const Calib_Params& params) auto obj_bb = model().objects[0]->bounding_box(); auto height = 5 * ((params.end - params.start) / params.step + 1); if (height < obj_bb.size().z()) { - std::array plane_pts = get_cut_plane(obj_bb, height); - cut(0, 0, plane_pts, ModelObjectCutAttribute::KeepLower); + cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower); } p->background_process.fff_print()->set_calib_params(params); @@ -9858,27 +9894,16 @@ void Plater::convert_unit(ConversionType conv_type) } } -// BBS: replace z with plane_points -void Plater::cut(size_t obj_idx, size_t instance_idx, std::array plane_points, ModelObjectCutAttributes attributes) +void Plater::apply_cut_object_to_model(size_t obj_idx, const ModelObjectPtrs& new_objects) { - wxCHECK_RET(obj_idx < p->model.objects.size(), "obj_idx out of bounds"); - auto *object = p->model.objects[obj_idx]; + model().delete_object(obj_idx); + sidebar().obj_list()->delete_object_from_list(obj_idx); - wxCHECK_RET(instance_idx < object->instances.size(), "instance_idx out of bounds"); - - if (! attributes.has(ModelObjectCutAttribute::KeepUpper) && ! attributes.has(ModelObjectCutAttribute::KeepLower)) - return; - - wxBusyCursor wait; - // BBS: replace z with plane_points - const auto new_objects = object->cut(instance_idx, plane_points, attributes); - - remove(obj_idx); - p->load_model_objects(new_objects); + // 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++) @@ -9888,62 +9913,10 @@ void Plater::cut(size_t obj_idx, size_t instance_idx, std::array plane size_t last_id = p->model.objects.size() - 1; for (size_t i = 0; i < new_objects.size(); ++i) selection.add_object((unsigned int)(last_id - i), i == 0); -} -// BBS -void Plater::segment(size_t obj_idx, size_t instance_idx, double smoothing_alpha, int segment_number) -{ - wxCHECK_RET(obj_idx < p->model.objects.size(), "obj_idx out of bounds"); - auto* object = p->model.objects[obj_idx]; - - wxCHECK_RET(instance_idx < object->instances.size(), "instance_idx out of bounds"); - - Plater::TakeSnapshot snapshot(this, "Segment"); - - wxBusyCursor wait; - // real process - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); - const size_t filament_cnt = print_tech != ptFFF ? 1 : preset_bundle.filament_presets.size(); - const auto new_objects = object->segment(instance_idx, filament_cnt, smoothing_alpha, segment_number); - - remove(obj_idx); - p->load_model_objects(new_objects); - - Selection& selection = p->get_selection(); - size_t last_id = p->model.objects.size() - 1; - for (size_t i = 0; i < new_objects.size(); ++i) - { - selection.add_object((unsigned int)(last_id - i), i == 0); - } -} - -// BBS -void Plater::merge(size_t obj_idx, std::vector& vol_indeces) -{ - wxCHECK_RET(obj_idx < p->model.objects.size(), "obj_idx out of bounds"); - auto* object = p->model.objects[obj_idx]; - - Plater::TakeSnapshot snapshot(this, "Merge"); - - wxBusyCursor wait; - // real process - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); - // BBS - const size_t filament_cnt = print_tech != ptFFF ? 1 : preset_bundle.filament_presets.size(); - - const auto new_objects = object->merge_volumes(vol_indeces); - - remove(obj_idx); - p->load_model_objects(new_objects); - - Selection& selection = p->get_selection(); - size_t last_id = p->model.objects.size() - 1; - for (size_t i = 0; i < new_objects.size(); ++i) - { - selection.add_object((unsigned int)(last_id - i), i == 0); - } + // UIThreadWorker w; + // arrange(w, true); + // w.wait_for_idle(); } void Plater::export_gcode(bool prefer_removable) diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 62c505f5ad..809a3ed801 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -1,3 +1,19 @@ +///|/ Copyright (c) Prusa Research 2018 - 2023 Tomáš Mészáros @tamasmeszaros, Oleksandra Iushchenko @YuSanka, Enrico Turri @enricoturri1966, David Kocík @kocikdav, Lukáš Hejl @hejllukas, Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena, Pavel Mikuš @Godrak, Filip Sykala @Jony01, Vojtěch Král @vojtechkral +///|/ +///|/ ported from lib/Slic3r/GUI/Plater.pm: +///|/ Copyright (c) Prusa Research 2016 - 2019 Vojtěch Bubník @bubnikv, Vojtěch Král @vojtechkral, Enrico Turri @enricoturri1966, Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Tomáš Mészáros @tamasmeszaros +///|/ Copyright (c) 2018 Martin Loidl @LoidlM +///|/ Copyright (c) 2017 Matthias Gazzari @qtux +///|/ Copyright (c) Slic3r 2012 - 2016 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2017 Joseph Lenox @lordofhyphens +///|/ Copyright (c) 2015 Daren Schwenke +///|/ Copyright (c) 2014 Mark Hindess +///|/ Copyright (c) 2012 Mike Sheldrake @mesheldrake +///|/ Copyright (c) 2012 Henrik Brix Andersen @henrikbrixandersen +///|/ Copyright (c) 2012 Sam Wong +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef slic3r_Plater_hpp_ #define slic3r_Plater_hpp_ @@ -25,6 +41,7 @@ #include "libslic3r/PrintBase.hpp" #include "libslic3r/calib.hpp" +#include "libslic3r/CutUtils.hpp" #define FILAMENT_SYSTEM_COLORS_NUM 16 @@ -41,8 +58,6 @@ class BuildVolume; enum class BuildVolume_Type : unsigned char; class Model; class ModelObject; -enum class ModelObjectCutAttribute : int; -using ModelObjectCutAttributes = enum_bitmask; class ModelInstance; class Print; class SLAPrint; @@ -323,12 +338,7 @@ public: void scale_selection_to_fit_print_volume(); void convert_unit(ConversionType conv_type); - // BBS: replace z with plane_points - void cut(size_t obj_idx, size_t instance_idx, std::array plane_points, ModelObjectCutAttributes attributes); - - // BBS: segment model with CGAL - void segment(size_t obj_idx, size_t instance_idx, double smoothing_alpha=0.5, int segment_number=5); - void merge(size_t obj_idx, std::vector& vol_indeces); + void apply_cut_object_to_model(size_t init_obj_idx, const ModelObjectPtrs& cut_objects); void send_to_printer(bool isall = false); void export_gcode(bool prefer_removable); @@ -742,6 +752,8 @@ private: void _calib_pa_tower(const Calib_Params& params); void _calib_pa_select_added_objects(); + void cut_horizontal(size_t obj_idx, size_t instance_idx, double z, ModelObjectCutAttributes attributes); + friend class SuppressBackgroundProcessingUpdate; }; diff --git a/src/slic3r/Utils/CalibUtils.cpp b/src/slic3r/Utils/CalibUtils.cpp index a496d1ff34..f1cafadd26 100644 --- a/src/slic3r/Utils/CalibUtils.cpp +++ b/src/slic3r/Utils/CalibUtils.cpp @@ -4,6 +4,7 @@ #include "../GUI/DeviceManager.hpp" #include "../GUI/Jobs/ProgressIndicator.hpp" #include "../GUI/PartPlate.hpp" +#include "libslic3r/CutUtils.hpp" #include "libslic3r/Model.hpp" @@ -133,7 +134,7 @@ bool CalibUtils::validate_input_flow_ratio(wxString flow_ratio, float* output_va return true; } -static void cut_model(Model &model, std::array plane_points, ModelObjectCutAttributes attributes) +static void cut_model(Model &model, double z, ModelObjectCutAttributes attributes) { size_t obj_idx = 0; size_t instance_idx = 0; @@ -142,7 +143,7 @@ static void cut_model(Model &model, std::array plane_points, ModelObje auto* object = model.objects[0]; - const auto new_objects = object->cut(instance_idx, plane_points, attributes); + const auto new_objects = Cut::cut_horizontal(object, instance_idx, z, attributes); model.delete_object(obj_idx); for (ModelObject *model_object : new_objects) { @@ -173,16 +174,6 @@ static void read_model_from_file(const std::string& input_file, Model& model) object->ensure_on_bed(); } -std::array get_cut_plane_points(const BoundingBoxf3 &bbox, const double &cut_height) -{ - std::array plane_pts; - plane_pts[0] = Vec3d(bbox.min(0), bbox.min(1), cut_height); - plane_pts[1] = Vec3d(bbox.max(0), bbox.min(1), cut_height); - plane_pts[2] = Vec3d(bbox.max(0), bbox.max(1), cut_height); - plane_pts[3] = Vec3d(bbox.min(0), bbox.max(1), cut_height); - return plane_pts; -} - void CalibUtils::calib_PA(const X1CCalibInfos& calib_infos, int mode, wxString& error_message) { DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); @@ -564,12 +555,7 @@ void CalibUtils::calib_temptue(const CalibInfo &calib_info, wxString &error_mess // add EPSILON offset to avoid cutting at the exact location where the flat surface is auto new_height = block_count * 10.0 + EPSILON; if (new_height < obj_bb.size().z()) { - std::array plane_pts; - plane_pts[0] = Vec3d(obj_bb.min(0), obj_bb.min(1), new_height); - plane_pts[1] = Vec3d(obj_bb.max(0), obj_bb.min(1), new_height); - plane_pts[2] = Vec3d(obj_bb.max(0), obj_bb.max(1), new_height); - plane_pts[3] = Vec3d(obj_bb.min(0), obj_bb.max(1), new_height); - cut_model(model, plane_pts, ModelObjectCutAttribute::KeepLower); + cut_model(model, new_height, ModelObjectCutAttribute::KeepLower); } } @@ -579,12 +565,7 @@ void CalibUtils::calib_temptue(const CalibInfo &calib_info, wxString &error_mess if (block_count > 0) { auto new_height = block_count * 10.0 + EPSILON; if (new_height < obj_bb.size().z()) { - std::array plane_pts; - plane_pts[0] = Vec3d(obj_bb.min(0), obj_bb.min(1), new_height); - plane_pts[1] = Vec3d(obj_bb.max(0), obj_bb.min(1), new_height); - plane_pts[2] = Vec3d(obj_bb.max(0), obj_bb.max(1), new_height); - plane_pts[3] = Vec3d(obj_bb.min(0), obj_bb.max(1), new_height); - cut_model(model, plane_pts, ModelObjectCutAttribute::KeepUpper); + cut_model(model, new_height, ModelObjectCutAttribute::KeepUpper); } } @@ -670,12 +651,7 @@ void CalibUtils::calib_max_vol_speed(const CalibInfo &calib_info, wxString &erro auto obj_bb = obj->bounding_box(); double height = (params.end - params.start + 1) / params.step; if (height < obj_bb.size().z()) { - std::array plane_pts; - plane_pts[0] = Vec3d(obj_bb.min(0), obj_bb.min(1), height); - plane_pts[1] = Vec3d(obj_bb.max(0), obj_bb.min(1), height); - plane_pts[2] = Vec3d(obj_bb.max(0), obj_bb.max(1), height); - plane_pts[3] = Vec3d(obj_bb.min(0), obj_bb.max(1), height); - cut_model(model, plane_pts, ModelObjectCutAttribute::KeepLower); + cut_model(model, height, ModelObjectCutAttribute::KeepLower); } auto new_params = params; @@ -731,12 +707,7 @@ void CalibUtils::calib_VFA(const CalibInfo &calib_info, wxString &error_message) auto obj_bb = model.objects[0]->bounding_box(); auto height = 5 * ((params.end - params.start) / params.step + 1); if (height < obj_bb.size().z()) { - std::array plane_pts; - plane_pts[0] = Vec3d(obj_bb.min(0), obj_bb.min(1), height); - plane_pts[1] = Vec3d(obj_bb.max(0), obj_bb.min(1), height); - plane_pts[2] = Vec3d(obj_bb.max(0), obj_bb.max(1), height); - plane_pts[3] = Vec3d(obj_bb.min(0), obj_bb.max(1), height); - cut_model(model, plane_pts, ModelObjectCutAttribute::KeepLower); + cut_model(model, height, ModelObjectCutAttribute::KeepLower); } else { error_message = _L("The start, end or step is not valid value."); @@ -790,8 +761,7 @@ void CalibUtils::calib_retraction(const CalibInfo &calib_info, wxString &error_m auto obj_bb = obj->bounding_box(); auto height = 1.0 + 0.4 + ((params.end - params.start)) / params.step; if (height < obj_bb.size().z()) { - std::array plane_pts = get_cut_plane_points(obj_bb, height); - cut_model(model, plane_pts, ModelObjectCutAttribute::KeepLower); + cut_model(model, height, ModelObjectCutAttribute::KeepLower); } DynamicPrintConfig full_config;