diff --git a/resources/icons/snap.svg b/resources/icons/snap.svg new file mode 100644 index 0000000000..0242c8e3bc --- /dev/null +++ b/resources/icons/snap.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index e600f343ca..480f42811e 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -40,6 +40,7 @@ #include "libslic3r/Geometry.hpp" #include "libslic3r/GCode/PostProcessor.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/CutUtils.hpp" #include "libslic3r/ModelArrange.hpp" #include "libslic3r/Platform.hpp" #include "libslic3r/Print.hpp" @@ -437,8 +438,11 @@ int CLI::run(int argc, char **argv) } #else // model.objects.front()->cut(0, m_config.opt_float("cut"), ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::FlipLower); - model.objects.front()->cut(0, Geometry::translation_transform(m_config.opt_float("cut") * Vec3d::UnitZ()), + 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 5aed978426..99d933ea87 100644 --- a/src/imgui/imconfig.h +++ b/src/imgui/imconfig.h @@ -152,6 +152,7 @@ namespace ImGui // const wchar_t MmuSegmentationMarker = 0x1F; const wchar_t PlugMarker = 0x1C; const wchar_t DowelMarker = 0x1D; + const wchar_t SnapMarker = 0x1E; // Do not forget use following letters only in wstring const wchar_t DocumentationButton = 0x2600; const wchar_t DocumentationHoverButton = 0x2601; diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 0de0b4e517..a31e4cc7c8 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -201,6 +201,8 @@ set(SLIC3R_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..1622af87e1 --- /dev/null +++ b/src/libslic3r/CutUtils.cpp @@ -0,0 +1,645 @@ + +#include "CutUtils.hpp" +#include "Geometry.hpp" +#include "libslic3r.h" +#include "Model.hpp" +#include "TriangleMeshSlicer.hpp" +#include "TriangleSelector.hpp" +#include "ObjectID.hpp" + + +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_no_scaling_factor()); + // 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_no_offset(); + 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); + } + 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); + + // Add Dowel-connectors as separate objects to cut_object_ptrs + if (cut_connectors_obj.size() >= 3) + for (size_t id = 2; id < cut_connectors_obj.size(); id++) + cut_object_ptrs.push_back(cut_connectors_obj[id]); + } + + // Now merge all model parts together: + merge_solid_parts_inside_object(cut_object_ptrs); + + finalize(cut_object_ptrs); + + 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; +} + +} // namespace Slic3r + diff --git a/src/libslic3r/CutUtils.hpp b/src/libslic3r/CutUtils.hpp new file mode 100644 index 0000000000..2c477a3e2b --- /dev/null +++ b/src/libslic3r/CutUtils.hpp @@ -0,0 +1,66 @@ +#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); + +}; // namespace Cut + +} // namespace Slic3r + +#endif /* slic3r_CutUtils_hpp_ */ diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 39f6ef98eb..2a8c78729b 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1280,64 +1280,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::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 ModelObject::apply_cut_connectors(const std::string& new_name) -{ - if (cut_connectors.empty()) - return; - - using namespace Geometry; - - size_t connector_id = cut_id.connectors_cnt(); - for (const CutConnector& connector : cut_connectors) { - TriangleMesh mesh = TriangleMesh(get_connector_mesh(connector.attribs)); - // Mesh will be centered when loading. - ModelVolume* new_volume = add_volume(std::move(mesh), ModelVolumeType::NEGATIVE_VOLUME); - - // Transform the new modifier to be aligned inside the instance - new_volume->set_transformation(translation_transform(connector.pos) * connector.rotation_m * - scale_transform(Vec3f(connector.radius, connector.radius, connector.height).cast())); - - new_volume->cut_info = { connector.attribs.type, connector.radius_tolerance, connector.height_tolerance }; - new_volume->name = new_name + "-" + std::to_string(++connector_id); - } - cut_id.increase_connectors_cnt(cut_connectors.size()); - - // delete all connectors - cut_connectors.clear(); -} - void ModelObject::invalidate_cut() { this->cut_id.invalidate(); @@ -1390,297 +1332,6 @@ void ModelVolume::reset_extra_facets() this->mmu_segmentation_facets.reset(); } -void ModelVolume::apply_tolerance() -{ - assert(cut_info.is_connector); - if (!cut_info.is_processed) - return; - - Vec3d sf = get_scaling_factor(); - - // 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); - - set_scaling_factor(sf); - - // correct offset in respect to the new depth - Vec3d rot_norm = Geometry::rotation_transform(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) - z_offset -= 0.05; // add small Z offset to better preview - - set_offset(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; -} - -void ModelObject::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 = 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); - - 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::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); -} - -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 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); -} -void ModelObject::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"); - 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); -} - -void ModelObject::reset_instance_transformation(ModelObject* object, size_t src_instance_idx, const Transform3d& cut_matrix, - bool place_on_cut/* = false*/, bool flip/* = false*/) -{ - using namespace Geometry; - - // Reset instance transformation except offset and Z-rotation - - for (size_t i = 0; i < object->instances.size(); ++i) { - auto& obj_instance = object->instances[i]; - const double rot_z = obj_instance->get_rotation().z(); - - Transformation inst_trafo = Transformation(obj_instance->get_transformation().get_matrix_no_scaling_factor()); - 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); - } -} - -ModelObjectPtrs ModelObject::cut(size_t instance, const Transform3d& cut_matrix, ModelObjectCutAttributes attributes) -{ - if (!attributes.has(ModelObjectCutAttribute::KeepUpper) && !attributes.has(ModelObjectCutAttribute::KeepLower)) - return {}; - - BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - start"; - - // Clone the object to duplicate instances, materials etc. - ModelObject* upper{ nullptr }; - if (attributes.has(ModelObjectCutAttribute::KeepUpper)) - clone_for_cut(&upper); - - ModelObject* lower{ nullptr }; - if (attributes.has(ModelObjectCutAttribute::KeepLower) && !attributes.has(ModelObjectCutAttribute::KeepAsParts)) - clone_for_cut(&lower); - - std::vector dowels; - - using namespace Geometry; - - // 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 = instances[instance]->get_transformation().get_matrix_no_offset(); - const Transformation cut_transformation = Transformation(cut_matrix); - const Transform3d inverse_cut_matrix = cut_transformation.get_rotation_matrix().inverse() * translation_transform(-1. * cut_transformation.get_offset()); - - for (ModelVolume* volume : 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, attributes, upper, lower); - else - process_connector_cut(volume, instance_matrix, cut_matrix, attributes, upper, lower, dowels); - } - else if (!volume->mesh().empty()) - process_solid_part_cut(volume, instance_matrix, cut_matrix, attributes, upper, lower); - } - - // Post-process cut parts - - ModelObjectPtrs res; - - if (attributes.has(ModelObjectCutAttribute::KeepAsParts) && !upper->volumes.empty()) { - reset_instance_transformation(upper, instance, cut_matrix); - res.push_back(upper); - } - else { - if (attributes.has(ModelObjectCutAttribute::KeepUpper) && !upper->volumes.empty()) { - reset_instance_transformation(upper, instance, cut_matrix, - attributes.has(ModelObjectCutAttribute::PlaceOnCutUpper), - attributes.has(ModelObjectCutAttribute::FlipUpper)); - res.push_back(upper); - } - - if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower->volumes.empty()) { - reset_instance_transformation(lower, instance, cut_matrix, - attributes.has(ModelObjectCutAttribute::PlaceOnCutLower), - attributes.has(ModelObjectCutAttribute::PlaceOnCutLower) || 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()); - dowel->name += "-Dowel-" + dowel->volumes[0]->name; - res.push_back(dowel); - } - } - } - - BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - end"; - - return res; -} /// /// Compare TriangleMeshes by Bounding boxes (mainly for sort) diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index bedd26436f..221033c521 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -224,6 +224,7 @@ private: enum class CutConnectorType : int { Plug , Dowel + , Snap , Undef }; @@ -316,10 +317,6 @@ enum class ModelVolumeType : int { SUPPORT_ENFORCER, }; -enum class ModelObjectCutAttribute : int { KeepUpper, KeepLower, KeepAsParts, FlipUpper, FlipLower, PlaceOnCutUpper, PlaceOnCutLower, CreateDowels, 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, @@ -461,29 +458,12 @@ public: size_t materials_count() const; size_t facets_count() const; size_t parts_count() const; - static indexed_triangle_set get_connector_mesh(CutConnectorAttributes connector_attributes); - void apply_cut_connectors(const std::string& name); // invalidate cut state for this object and its connectors/volumes void invalidate_cut(); // delete volumes which are marked as connector for this object void delete_connectors(); void clone_for_cut(ModelObject **obj); -private: - void process_connector_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix, - ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, - std::vector& dowels); - 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, - ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower); -public: - static void reset_instance_transformation(ModelObject* object, size_t src_instance_idx, const Transform3d& cut_matrix, - bool place_on_cut = false, bool flip = false); - - ModelObjectPtrs cut(size_t instance, const Transform3d&cut_matrix, ModelObjectCutAttributes attributes); void split(ModelObjectPtrs*new_objects); void merge(); // Support for non-uniform scaling of instances. If an instance is rotated by angles, which are not multiples of ninety degrees, @@ -777,6 +757,7 @@ public: // It contains information about connetors struct CutInfo { + bool is_from_upper{ true }; bool is_connector{ false }; bool is_processed{ true }; CutConnectorType connector_type{ CutConnectorType::Plug }; @@ -794,6 +775,7 @@ public: void set_processed() { is_processed = true; } 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); @@ -801,6 +783,9 @@ public: }; CutInfo cut_info; + 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(); } @@ -846,7 +831,6 @@ public: bool is_the_only_one_part() const; // behave like an object t_model_material_id material_id() const { return m_material_id; } void reset_extra_facets(); - void apply_tolerance(); void set_material_id(t_model_material_id material_id); ModelMaterial* material() const; void set_material(t_model_material_id material_id, const ModelMaterial &material); diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index 4bf9fd486a..236fa22c51 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -1262,6 +1262,127 @@ indexed_triangle_set its_make_frustum_dowel(double radius, double h, int sectorC 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 0f43f9d58d..4b524402c0 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -321,6 +321,7 @@ 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/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index c2a4e752f6..9f09619c88 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2685,6 +2685,9 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re if (curr_gizmo != nullptr) curr_gizmo->unregister_raycasters_for_picking(); m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::Gizmo); + if (curr_gizmo != nullptr && !m_selection.is_empty()) + curr_gizmo->register_raycasters_for_picking(); + m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::FallbackGizmo); if (curr_gizmo != nullptr && !m_selection.is_empty()) curr_gizmo->register_raycasters_for_picking(); @@ -5740,6 +5743,7 @@ void GLCanvas3D::_picking_pass() break; } case SceneRaycaster::EType::Gizmo: + case SceneRaycaster::EType::FallbackGizmo: { const Size& cnv_size = get_canvas_size(); const bool inside = 0 <= m_mouse.position.x() && m_mouse.position.x() < cnv_size.get_width() && @@ -5772,6 +5776,7 @@ void GLCanvas3D::_picking_pass() { case SceneRaycaster::EType::Bed: { object_type = "Bed"; break; } case SceneRaycaster::EType::Gizmo: { object_type = "Gizmo element"; break; } + case SceneRaycaster::EType::FallbackGizmo: { object_type = "Gizmo2 element"; break; } case SceneRaycaster::EType::Volume: { if (m_volumes.volumes[hit.raycaster_id]->is_wipe_tower) @@ -5826,6 +5831,8 @@ void GLCanvas3D::_picking_pass() add_strings_row_to_table("Volumes", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text)); sprintf(buf, "%d (%d)", (int)m_scene_raycaster.gizmos_count(), (int)m_scene_raycaster.active_gizmos_count()); add_strings_row_to_table("Gizmo elements", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + sprintf(buf, "%d (%d)", (int)m_scene_raycaster.fallback_gizmos_count(), (int)m_scene_raycaster.active_fallback_gizmos_count()); + add_strings_row_to_table("Gizmo2 elements", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text)); ImGui::EndTable(); } @@ -5843,6 +5850,20 @@ void GLCanvas3D::_picking_pass() } } + std::vector>* gizmo2_raycasters = m_scene_raycaster.get_raycasters(SceneRaycaster::EType::FallbackGizmo); + if (gizmo2_raycasters != nullptr && !gizmo2_raycasters->empty()) { + ImGui::Separator(); + imgui.text("Gizmo2 raycasters IDs:"); + if (ImGui::BeginTable("Gizmo2Raycasters", 3)) { + for (size_t i = 0; i < gizmo2_raycasters->size(); ++i) { + add_strings_row_to_table(std::to_string(i), ImGuiWrapper::COL_ORANGE_LIGHT, + std::to_string(SceneRaycaster::decode_id(SceneRaycaster::EType::FallbackGizmo, (*gizmo2_raycasters)[i]->get_id())), ImGui::GetStyleColorVec4(ImGuiCol_Text), + to_string(Geometry::Transformation((*gizmo2_raycasters)[i]->get_transform()).get_offset()), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + } + ImGui::EndTable(); + } + } + imgui.end(); #endif // ENABLE_RAYCAST_PICKING_DEBUG } diff --git a/src/slic3r/GUI/GalleryDialog.cpp b/src/slic3r/GUI/GalleryDialog.cpp index a08673e235..5ac51d54f7 100644 --- a/src/slic3r/GUI/GalleryDialog.cpp +++ b/src/slic3r/GUI/GalleryDialog.cpp @@ -134,7 +134,12 @@ GalleryDialog::GalleryDialog(wxWindow* parent) : } GalleryDialog::~GalleryDialog() -{ +{ + // From wxWidgets docs: + // The method void wxListCtrl::SetImageList(wxImageList* imageList, int which) + // does not take ownership of the image list, you have to delete it yourself. + if (m_image_list) + delete m_image_list; } int GalleryDialog::show(bool show_from_menu) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 36576e9494..9d23a51808 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -35,6 +35,9 @@ 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; @@ -183,15 +186,16 @@ GLGizmoCut3D::GLGizmoCut3D(GLCanvas3D& parent, const std::string& icon_filename, , m_connector_style (int(CutConnectorStyle::Prism)) , m_connector_shape_id (int(CutConnectorShape::Circle)) { -// m_modes = { _u8L("Planar"), _u8L("Grid") + m_modes = { _u8L("Planar"), _u8L("Tongue and Groove")//, _u8L("Grid") // , _u8L("Radial"), _u8L("Modular") -// }; + }; m_connector_modes = { _u8L("Auto"), _u8L("Manual") }; std::map connetor_types = { {ImGui::PlugMarker , _u8L("Plug") }, - {ImGui::DowelMarker, _u8L("Dowel") }, + {ImGui::DowelMarker, _u8L("Dowel") }, + {ImGui::SnapMarker, _u8L("Snap") }, }; for (auto connector : connetor_types) { std::string type_label = " " + connector.second + " "; @@ -222,9 +226,13 @@ GLGizmoCut3D::GLGizmoCut3D(GLCanvas3D& parent, const std::string& icon_filename, {"Shape" , _u8L("Shape")}, {"Depth" , _u8L("Depth")}, {"Size" , _u8L("Size")}, + {"Groove" , _u8L("Groove")}, + {"Width" , _u8L("Width")}, + {"Flaps Angle" , _u8L("Flaps Angle")}, + {"Groove Angle" , _u8L("Groove Angle")}, }; - update_connector_shape(); +// update_connector_shape(); } std::string GLGizmoCut3D::get_tooltip() const @@ -249,13 +257,17 @@ std::string GLGizmoCut3D::get_tooltip() const return tooltip; } - if (!m_dragging && m_hover_id == CutPlane) + 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)) { - std::string axis = m_hover_id == X ? "X" : "Y"; + 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("°"); } @@ -283,6 +295,14 @@ bool GLGizmoCut3D::on_mouse(const wxMouseEvent &mouse_event) 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()) @@ -297,7 +317,7 @@ bool GLGizmoCut3D::on_mouse(const wxMouseEvent &mouse_event) // disable / enable current contour Vec3d pos; Vec3d pos_world; - m_was_contour_selected = unproject_on_cut_plane(mouse_pos.cast(), pos, pos_world, false); + 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. @@ -311,8 +331,15 @@ bool GLGizmoCut3D::on_mouse(const wxMouseEvent &mouse_event) flip_cut_plane(); } - if (m_part_selection.valid()) - m_parent.toggle_model_objects_visibility(false); + 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; } @@ -353,7 +380,8 @@ bool GLGizmoCut3D::on_mouse(const wxMouseEvent &mouse_event) return true; } else if (mouse_event.RightDown()) { - if (! m_connectors_editing && mouse_event.GetModifiers() == wxMOD_NONE) { + 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(); @@ -466,13 +494,43 @@ void GLGizmoCut3D::set_center(const Vec3d& center, bool update_tbb /*=false*/) 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(), _L("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(); + //if (is_changed) + // update_connector_shape(); return is_changed; } @@ -497,7 +555,7 @@ bool GLGizmoCut3D::render_double_input(const std::string& label, double& value_i return !is_approx(old_val, value); } -bool GLGizmoCut3D::render_slider_double_input(const std::string& label, float& value_in, float& tolerance_in) +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*/) { constexpr float UndefMinVal = -0.1f; const float f_mm_to_in = static_cast(ObjectManipulation::mm_to_in); @@ -516,23 +574,29 @@ bool GLGizmoCut3D::render_slider_double_input(const std::string& label, float& v 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(ObjectManipulation::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; ImGuiWrapper::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, 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 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; } @@ -568,7 +632,7 @@ bool GLGizmoCut3D::render_connect_type_radio_button(CutConnectorType type) 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(); +// update_connector_shape(); return true; } return false; @@ -606,6 +670,232 @@ bool GLGizmoCut3D::render_reset_button(const std::string& label_id, const std::s 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()) @@ -623,19 +913,16 @@ void GLGizmoCut3D::render_cut_plane() shader->start_using(); const Camera& camera = wxGetApp().plater()->get_camera(); - 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); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - if (can_perform_cut() && has_valid_contour()) { - if (m_hover_id == CutPlane) - m_plane.model.set_color({ 0.9f, 0.9f, 0.9f, 0.5f }); - else - m_plane.model.set_color({ 0.8f, 0.8f, 0.8f, 0.5f }); - } - else - m_plane.model.set_color({ 1.0f, 0.8f, 0.8f, 0.5f }); + 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)); @@ -644,11 +931,6 @@ void GLGizmoCut3D::render_cut_plane() shader->stop_using(); } -static double get_grabber_mean_size(const BoundingBoxf3& bb) -{ - return (bb.size().x() + bb.size().y() + bb.size().z()) / 30.; -} - static double get_half_size(double size) { return std::max(size * 0.35, 0.05); @@ -703,8 +985,10 @@ void GLGizmoCut3D::render_rotation_snapping(GrabberID axis, const ColorRGBA& col if (axis == X) view_model_matrix = view_model_matrix * rotation_transform(0.5 * PI * Vec3d::UnitY()) * rotation_transform(-PI * Vec3d::UnitZ()); - else + 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()); @@ -724,9 +1008,9 @@ void GLGizmoCut3D::render_rotation_snapping(GrabberID axis, const ColorRGBA& col line_shader->stop_using(); } -void GLGizmoCut3D::render_grabber_connection(const ColorRGBA& color, Transform3d view_matrix) +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, m_grabber_connection_len)); + 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); }; @@ -742,9 +1026,9 @@ void GLGizmoCut3D::render_cut_plane_grabbers() const double mean_size = get_grabber_mean_size(m_bounding_box); double size; - const bool dragging_by_cut_plane = m_dragging && m_hover_id == CutPlane; + const bool no_xy_dragging = m_dragging && m_hover_id == CutPlane; - if (!dragging_by_cut_plane) { + if (!no_xy_dragging && m_hover_id != CutPlaneZRotation && m_hover_id != CutPlaneXMove && m_hover_id != CutPlaneYMove) { render_grabber_connection(GRABBER_COLOR, view_matrix); // render sphere grabber @@ -755,11 +1039,11 @@ void GLGizmoCut3D::render_cut_plane_grabbers() render_model(m_sphere.model, color, view_matrix * translation_transform(m_grabber_connection_len * Vec3d::UnitZ()) * scale_transform(size)); } - const bool no_one_grabber_hovered = !m_dragging && (m_hover_id < 0 || m_hover_id == CutPlane); + const bool no_xy_grabber_hovered = !m_dragging && (m_hover_id < 0 || m_hover_id == CutPlane); // render X grabber - if (no_one_grabber_hovered || m_hover_id == X) + 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); @@ -778,7 +1062,7 @@ void GLGizmoCut3D::render_cut_plane_grabbers() // render Y grabber - if (no_one_grabber_hovered || m_hover_id == Y) + 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); @@ -792,7 +1076,73 @@ void GLGizmoCut3D::render_cut_plane_grabbers() 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)); + 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)); + } } } @@ -832,21 +1182,52 @@ bool GLGizmoCut3D::on_init() void GLGizmoCut3D::on_load(cereal::BinaryInputArchive& ar) { - ar( m_keep_upper, m_keep_lower, m_rotate_lower, m_rotate_upper, m_hide_cut_plane, m_mode, m_connectors_editing, - m_ar_plane_center, m_rotation_m); + 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_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 @@ -854,11 +1235,18 @@ 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); - m_parent.set_color_clip_plane_colors({ UPPER_PART_COLOR , LOWER_PART_COLOR }); update_bb(); m_connectors_editing = !m_selected.empty(); @@ -891,7 +1279,9 @@ void GLGizmoCut3D::on_set_state() void GLGizmoCut3D::on_register_raycasters_for_picking() { - assert(m_raycasters.empty()); + // 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); @@ -913,7 +1303,19 @@ void GLGizmoCut3D::on_register_raycasters_for_picking() m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Z, *m_sphere.mesh_raycaster, Transform3d::Identity())); - m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlane, *m_plane.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(); @@ -922,6 +1324,7 @@ void GLGizmoCut3D::on_register_raycasters_for_picking() 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); @@ -1005,13 +1408,52 @@ void GLGizmoCut3D::update_raycasters_for_picking_transform() 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 = 1.25 * size * Vec3d::UnitZ(); 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() { } @@ -1060,8 +1502,8 @@ Vec3d GLGizmoCut3D::mouse_position_in_local_plane(GrabberID axis, const Linef3& m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); break; } - default: case Z: + default: { // no rotation applied break; @@ -1074,17 +1516,21 @@ Vec3d GLGizmoCut3D::mouse_position_in_local_plane(GrabberID axis, const Linef3& return transform(mouse_ray, m).intersect_plane(0.0); } -void GLGizmoCut3D::dragging_grabber_z(const GLGizmoBase::UpdateData &data) +void GLGizmoCut3D::dragging_grabber_move(const GLGizmoBase::UpdateData &data) { - const Vec3d grabber_init_pos = (m_hover_id == CutPlane ? 0. : m_grabber_connection_len) * Vec3d::UnitZ(); - const Vec3d starting_drag_position = translation_transform(m_plane_center) * m_rotation_m * grabber_init_pos; - double projection = 0.0; + 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; - Vec3d starting_vec = m_rotation_m * Vec3d::UnitZ(); + 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 throught the starting position - // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebric form + // 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 = data.mouse_ray.a + (starting_drag_position - data.mouse_ray.a).dot(mouse_dir) * mouse_dir; @@ -1108,7 +1554,7 @@ void GLGizmoCut3D::dragging_grabber_z(const GLGizmoBase::UpdateData &data) m_was_cut_plane_dragged = true; } -void GLGizmoCut3D::dragging_grabber_xy(const GLGizmoBase::UpdateData &data) +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)); @@ -1135,14 +1581,14 @@ void GLGizmoCut3D::dragging_grabber_xy(const GLGizmoBase::UpdateData &data) if (is_approx(theta, two_pi)) theta = 0.0; - if (m_hover_id == X) + 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] = theta; + 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()); @@ -1175,13 +1621,16 @@ void GLGizmoCut3D::on_dragging(const UpdateData& data) { if (m_hover_id < 0) return; - if (m_hover_id == Z || m_hover_id == CutPlane) - dragging_grabber_z(data); - else if (m_hover_id == X || m_hover_id == Y) - dragging_grabber_xy(data); + 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() @@ -1190,23 +1639,26 @@ void GLGizmoCut3D::on_start_dragging() if (m_hover_id >= m_connectors_group_id && m_connector_mode == CutConnectorMode::Manual) Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Move connector"), UndoRedo::SnapshotType::GizmoAction); - if (m_hover_id == X || m_hover_id == Y) + 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) { + 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(), _L("Rotate cut plane"), UndoRedo::SnapshotType::GizmoAction); m_start_dragging_m = m_rotation_m; } - else if (m_hover_id == Z || m_hover_id == CutPlane) { + 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(), _L("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(); } @@ -1220,7 +1672,8 @@ void GLGizmoCut3D::set_center_pos(const Vec3d& center_pos, bool update_tbb /*=fa bool can_set_center_pos = false; { - if (tbb.max.z() > -.5 && tbb.min.z() < .5) + 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(); @@ -1286,8 +1739,14 @@ void GLGizmoCut3D::update_bb() 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; @@ -1298,6 +1757,8 @@ void GLGizmoCut3D::update_bb() 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; @@ -1307,9 +1768,15 @@ void GLGizmoCut3D::update_bb() 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(); @@ -1337,10 +1804,17 @@ void GLGizmoCut3D::init_picking_models() 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 = its_make_frustum_dowel((double)m_cut_plane_radius_koef * m_radius, cp_width, m_cut_plane_as_circle ? 180 : 4); + 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))); } @@ -1383,20 +1857,25 @@ void GLGizmoCut3D::render_clipper_cut() ::glEnable(GL_DEPTH_TEST); } - -GLGizmoCut3D::PartSelection::PartSelection(const ModelObject* mo, const Transform3d& cut_matrix, int instance_idx_in, const Vec3d& center, const Vec3d& normal, const CommonGizmosDataObjects::ObjectClipper& oc) +void GLGizmoCut3D::PartSelection::add_object(const ModelObject* object) { m_model = Model(); - m_model.add_object(*mo); - ModelObjectPtrs cut_part_ptrs = m_model.objects.front()->cut(instance_idx_in, cut_matrix, - ModelObjectCutAttribute::KeepUpper | - ModelObjectCutAttribute::KeepLower | - ModelObjectCutAttribute::KeepAsParts); - assert(cut_part_ptrs.size() == 1); - m_model = Model(); - m_model.add_object(*cut_part_ptrs.front()); + m_model.add_object(*object); - m_instance_idx = instance_idx_in; + 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; @@ -1467,6 +1946,26 @@ GLGizmoCut3D::PartSelection::PartSelection(const ModelObject* mo, const Transfor 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()) @@ -1563,6 +2062,16 @@ bool GLGizmoCut3D::PartSelection::is_one_object() const }); } +std::vector 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) { @@ -1617,6 +2126,8 @@ void GLGizmoCut3D::on_render() m_c->selection_info()->set_use_shift(true); } + // check objects visibility + toggle_model_objects_visibility(); update_clipper(); @@ -1648,9 +2159,7 @@ void GLGizmoCut3D::render_debug_input_window(float x) return; m_imgui->begin(wxString("DEBUG")); - ImVec2 pos = ImGui::GetWindowPos(); - pos.x = x; - ImGui::SetWindowPos(pos, ImGuiCond_Always); + m_imgui->end(); /* static bool hide_clipped = false; static bool fill_cut = false; @@ -1775,22 +2284,30 @@ void GLGizmoCut3D::render_connectors_input_window(CutConnectors &connectors) 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::Dowel); - 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_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(); - 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_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(); - if (render_slider_double_input(m_labels_map["Depth"], m_connector_depth_ratio, m_connector_depth_ratio_tolerance)) + 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; @@ -1806,6 +2323,45 @@ void GLGizmoCut3D::render_connectors_input_window(CutConnectors &connectors) connectors[idx].radius_tolerance = 0.5f * m_connector_size_tolerance; }); + if (m_connector_type == CutConnectorType::Snap) { + + const std::string format = "%.0f %%"; + + bool is_changed = false; + { + const std::string label = _u8L("Bulge"); + ImGuiWrapper::text(label); + + ImGui::SameLine(m_label_width); + ImGui::PushItemWidth(m_control_width * 0.7f); + + float val = m_snap_bulge_proportion *100.f; + if (m_imgui->slider_float(("##snap_" + label).c_str(), &val, 5.f, 100.f * m_snap_space_proportion, format.c_str(), 1.f, true, _u8L("Bulge proportion related to radius"))) { + m_snap_bulge_proportion = val * 0.01f; + is_changed = true; + } + } + + { + const std::string label = _u8L("Space"); + ImGuiWrapper::text(label); + + ImGui::SameLine(m_label_width); + ImGui::PushItemWidth(m_control_width * 0.7f); + + float val = m_snap_space_proportion *100.f; + if (m_imgui->slider_float(("##snap_" + label).c_str(), &val, 10.f, 50.f, format.c_str(), 1.f, true, _u8L("Space proportion related to radius"))) { + m_snap_space_proportion = val * 0.01f; + is_changed = true; + } + } + + if (is_changed) { + update_connector_shape(); + update_raycasters_for_picking(); + } + } + ImGui::Separator(); if (m_imgui->button(_L("Confirm connectors"))) { @@ -1882,30 +2438,50 @@ void GLGizmoCut3D::flip_cut_plane() 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(); - 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()); + 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() { - reset_cut_by_contours(); - const Selection& selection = m_parent.get_selection(); const ModelObjectPtrs& model_objects = selection.get_model()->objects; - wxBusyCursor wait; const int instance_idx = selection.get_instance_idx(); + if (instance_idx < 0) + return; const int object_idx = selection.get_object_idx(); - m_part_selection = PartSelection(model_objects[object_idx], get_cut_matrix(selection), instance_idx, m_plane_center, m_cut_normal, *m_c->object_clipper()); - m_parent.toggle_model_objects_visibility(false); + 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*/) @@ -1950,12 +2526,100 @@ void GLGizmoCut3D::render_color_marker(float size, const ImU32& color) ImGuiWrapper::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(), format_wxstr("%1%: %2%", _L("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(), format_wxstr("%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(); + } +} + +void GLGizmoCut3D::render_groove_angle_input(const std::string& label, float& in_val, const float& init_val, float min_val, float max_val) +{ + bool is_changed{ false }; + + ImGuiWrapper::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(("##groove_" + 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) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), format_wxstr("%1%: %2%", _L("Groove change"), label), UndoRedo::SnapshotType::GizmoAction); + m_imgui->get_last_slider_status().invalidate_snapshot(); + 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(("##groove_" + label + act_name).c_str(), act_name)) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), format_wxstr("%1%: %2%", act_name, label), UndoRedo::SnapshotType::GizmoAction); + in_val = init_val; + 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(); + } +} + + void GLGizmoCut3D::render_cut_plane_input_window(CutConnectors &connectors) { - // WIP : cut plane mode - // render_combo(_u8L("Mode"), m_modes, m_mode); - - if (m_mode == size_t(CutMode::cutPlanar)) { +// if (m_mode == size_t(CutMode::cutPlanar)) { + CutMode mode = CutMode(m_mode); + if (mode == CutMode::cutPlanar || mode == CutMode::cutTongueAndGroove) { ImGui::AlignTextToFramePadding(); ImGuiWrapper::text(wxString(ImGui::InfoMarkerSmall)); ImGui::SameLine(); @@ -1963,6 +2627,13 @@ void GLGizmoCut3D::render_cut_plane_input_window(CutConnectors &connectors) 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(); @@ -1971,8 +2642,6 @@ void GLGizmoCut3D::render_cut_plane_input_window(CutConnectors &connectors) render_move_center_input(Z); ImGui::SameLine(); - const bool has_connectors = !connectors.empty(); - 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); wxString act_name = _L("Reset cutting plane"); @@ -1984,23 +2653,34 @@ void GLGizmoCut3D::render_cut_plane_input_window(CutConnectors &connectors) // render_flip_plane_button(); - add_vertical_scaled_interval(0.75f); + 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(); + 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); + ImGui::SameLine(1.5f * m_control_width); - m_imgui->disabled_begin(is_cut_plane_init && !has_connectors); - act_name = _L("Reset cut"); - if (m_imgui->button(act_name, _L("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(); + m_imgui->disabled_begin(is_cut_plane_init && !has_connectors); + act_name = _L("Reset cut"); + if (m_imgui->button(act_name, _L("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(); + ImGuiWrapper::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["Flaps 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(); @@ -2069,7 +2749,7 @@ void GLGizmoCut3D::render_cut_plane_input_window(CutConnectors &connectors) add_vertical_scaled_interval(0.75f); - m_imgui->disabled_begin(has_connectors || m_part_selection.valid()); + m_imgui->disabled_begin(has_connectors || m_part_selection.valid() || mode == CutMode::cutTongueAndGroove); ImGuiWrapper::text(_L("Cut into") + ":"); if (m_part_selection.valid()) @@ -2094,7 +2774,7 @@ void GLGizmoCut3D::render_cut_plane_input_window(CutConnectors &connectors) ImGui::Separator(); - m_imgui->disabled_begin(!m_is_contour_changed && !can_perform_cut()); + m_imgui->disabled_begin(!can_perform_cut()); if(m_imgui->button(_L("Perform cut"))) perform_cut(m_parent.get_selection()); m_imgui->disabled_end(); @@ -2190,8 +2870,6 @@ void GLGizmoCut3D::init_input_window_data(CutConnectors &connectors) void GLGizmoCut3D::render_input_window_warning() const { - if (m_is_contour_changed) - return; if (! m_invalid_connectors_idxs.empty()) { wxString out = wxString(ImGui::WarningMarkerSmall) + _L("Invalid connectors detected") + ":"; if (m_info_stats.outside_cut_contour > size_t(0)) @@ -2208,6 +2886,8 @@ void GLGizmoCut3D::render_input_window_warning() const m_imgui->text(wxString(ImGui::WarningMarkerSmall) + _L("Select at least one object to keep after cutting.")); if (!has_valid_contour()) m_imgui->text(wxString(ImGui::WarningMarkerSmall) + _L("Cut plane is placed out of object")); + else if (!has_valid_groove()) + m_imgui->text(wxString(ImGui::WarningMarkerSmall) + _L("Cut plane with groove is invalid")); } void GLGizmoCut3D::on_render_input_window(float x, float y, float bottom_limit) @@ -2317,6 +2997,8 @@ 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) @@ -2334,11 +3016,31 @@ void GLGizmoCut3D::check_and_update_connectors_state() } } +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 auto 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 (m_is_contour_changed || cut_line_processing() || + if (cut_line_processing() || + CutMode(m_mode) != CutMode::cutPlanar || m_connector_mode == CutConnectorMode::Auto || !m_c->selection_info()) return; @@ -2414,10 +3116,51 @@ 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;// has_valid_contour(); + 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 @@ -2428,6 +3171,8 @@ bool GLGizmoCut3D::has_valid_contour() const 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(); @@ -2444,7 +3189,7 @@ void GLGizmoCut3D::apply_connectors_in_model(ModelObject* mo, int &dowels_count) connector.pos += m_cut_normal * 0.5 * double(connector.height); } } - mo->apply_cut_connectors(_u8L("Connector")); + apply_cut_connectors(mo, _u8L("Connector")); } } @@ -2518,7 +3263,9 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) // This shall delete the part selection class and deallocate the memory. ScopeGuard part_selection_killer([this]() { m_part_selection = PartSelection(); }); - const bool cut_by_contour = m_part_selection.valid(); + 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; @@ -2532,8 +3279,6 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) wxBusyCursor wait; - const Transform3d cut_matrix = get_cut_matrix(selection); - 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) | @@ -2542,129 +3287,20 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) 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_mo->cut_id.id().invalid(), ModelObjectCutAttribute::InvalidateCutInfo); + 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); - ModelObjectPtrs cut_object_ptrs; - if (cut_by_contour) { - // Clone the object to duplicate instances, materials etc. - ModelObject* upper{ nullptr }; - if (m_keep_upper) cut_mo->clone_for_cut(&upper); - ModelObject* lower{ nullptr }; - if (m_keep_lower) cut_mo->clone_for_cut(&lower); - - auto add_cut_objects = [this, &instance_idx, &cut_matrix](ModelObjectPtrs& cut_objects, ModelObject* upper, ModelObject* lower) { - if (upper && !upper->volumes.empty()) { - ModelObject::reset_instance_transformation(upper, instance_idx, cut_matrix, m_place_on_cut_upper, m_rotate_upper); - cut_objects.push_back(upper); - } - if (lower && !lower->volumes.empty()) { - ModelObject::reset_instance_transformation(lower, instance_idx, cut_matrix, m_place_on_cut_lower, m_place_on_cut_lower || m_rotate_lower); - cut_objects.push_back(lower); - } - }; - - const size_t cut_parts_cnt = m_part_selection.parts().size(); - bool has_modifiers = false; - - // Distribute SolidParts to the Upper/Lower object - for (size_t id = 0; id < cut_parts_cnt; ++id) { - if (m_part_selection.parts()[id].is_modifier) - has_modifiers = true; // modifiers will be added later to the related parts - else if (ModelObject* obj = (m_part_selection.parts()[id].selected ? upper : lower)) - obj->add_volume(*(cut_mo->volumes[id])); - } - - if (has_modifiers) { - // Distribute Modifiers to the Upper/Lower object - auto upper_bb = upper ? upper->instance_bounding_box(instance_idx) : BoundingBoxf3(); - auto lower_bb = lower ? lower->instance_bounding_box(instance_idx) : BoundingBoxf3(); - const Transform3d inst_matrix = cut_mo->instances[instance_idx]->get_transformation().get_matrix(); - - for (size_t id = 0; id < cut_parts_cnt; ++id) - if (m_part_selection.parts()[id].is_modifier) { - ModelVolume* vol = cut_mo->volumes[id]; - auto bb = vol->mesh().transformed_bounding_box(inst_matrix * vol->get_matrix()); - // Don't add modifiers which are not intersecting with solid parts - if (upper_bb.intersects(bb)) - upper->add_volume(*vol); - if (lower_bb.intersects(bb)) - lower->add_volume(*vol); - } - } - - 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 - add_cut_objects(cut_object_ptrs, upper, lower); - } - 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 - const ModelObjectPtrs cut_connectors_obj = cut_mo->cut(instance_idx, get_cut_matrix(selection), attributes); - 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 - add_cut_objects(cut_object_ptrs, upper, lower); - - // Add Dowel-connectors as separate objects to cut_object_ptrs - if (cut_connectors_obj.size() >= 3) - for (size_t id = 2; id < cut_connectors_obj.size(); id++) - cut_object_ptrs.push_back(cut_connectors_obj[id]); - } - - // Now merge all model parts together: - { - for (ModelObject* mo : cut_object_ptrs) { - 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); - } - } - } - } - } - else - cut_object_ptrs = cut_mo->cut(instance_idx, cut_matrix, attributes); - + 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->cut(object_idx, cut_object_ptrs); + plater->apply_cut_object_to_model(object_idx, new_objects); synchronize_model_after_cut(plater->model(), cut_id); } @@ -2672,7 +3308,7 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) // 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_disabled_contour/* = true*/) +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(); @@ -2709,6 +3345,7 @@ bool GLGizmoCut3D::unproject_on_cut_plane(const Vec2d& mouse_position, Vec3d& po } }*/ + 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); @@ -2750,13 +3387,15 @@ void GLGizmoCut3D::reset_connectors() void GLGizmoCut3D::init_connector_shapes() { - for (const CutConnectorType& type : {CutConnectorType::Dowel, CutConnectorType::Plug}) + 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 }; - const indexed_triangle_set its = ModelObject::get_connector_mesh(attribs); + 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))); } @@ -2767,9 +3406,18 @@ void GLGizmoCut3D::update_connector_shape() { CutConnectorAttributes attribs = { m_connector_type, CutConnectorStyle(m_connector_style), CutConnectorShape(m_connector_shape_id) }; - const indexed_triangle_set its = ModelObject::get_connector_mesh(attribs); - m_connector_mesh.clear(); - m_connector_mesh = TriangleMesh(its); + 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 @@ -2800,6 +3448,8 @@ bool GLGizmoCut3D::process_cut_line(SLAGizmoEventType action, const Vec2d& mouse } if (cut_line_processing()) { + if (CutMode(m_mode) == CutMode::cutTongueAndGroove) + m_groove_editing = true; reset_cut_by_contours(); m_line_end = pt; @@ -2831,6 +3481,11 @@ bool GLGizmoCut3D::process_cut_line(SLAGizmoEventType action, const Vec2d& mouse 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(); @@ -3026,5 +3681,69 @@ void GLGizmoCut3D::data_changed(bool is_serializing) } + + +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 * + 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 51174a51af..aa8ed04dd9 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp @@ -7,12 +7,14 @@ #include "slic3r/GUI/I18N.hpp" #include "libslic3r/TriangleMesh.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 { @@ -29,6 +31,9 @@ class GLGizmoCut3D : public GLGizmoBase Y, Z, CutPlane, + CutPlaneZRotation, + CutPlaneXMove, + CutPlaneYMove, Count, }; @@ -54,6 +59,7 @@ class GLGizmoCut3D : public GLGizmoBase 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 }; @@ -78,6 +84,7 @@ class GLGizmoCut3D : public GLGizmoBase PickingModel m_plane; PickingModel m_sphere; PickingModel m_cone; + PickingModel m_cube; std::map m_shapes; std::vector> m_raycasters; @@ -111,6 +118,16 @@ class GLGizmoCut3D : public GLGizmoBase 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 }; @@ -127,7 +144,6 @@ class GLGizmoCut3D : public GLGizmoBase float m_contour_width{ 0.4f }; float m_cut_plane_radius_koef{ 1.5f }; - bool m_is_contour_changed{ false }; float m_shortcut_label_width{ -1.f }; mutable std::vector m_selected; // which pins are currently selected @@ -139,10 +155,14 @@ class GLGizmoCut3D : public GLGizmoBase 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 { @@ -161,6 +181,8 @@ class GLGizmoCut3D : public GLGizmoBase 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; @@ -171,6 +193,8 @@ class GLGizmoCut3D : public GLGizmoBase std::vector m_contour_points; // Debugging std::vector> m_debug_pts; // Debugging + + void add_object(const ModelObject* object); }; PartSelection m_part_selection; @@ -180,7 +204,8 @@ class GLGizmoCut3D : public GLGizmoBase enum class CutMode { cutPlanar - , cutGrig + , cutTongueAndGroove + //, cutGrig //,cutRadial //,cutModular }; @@ -190,7 +215,7 @@ class GLGizmoCut3D : public GLGizmoBase , Manual }; -// std::vector m_modes; + std::vector m_modes; size_t m_mode{ size_t(CutMode::cutPlanar) }; std::vector m_connector_modes; @@ -215,7 +240,7 @@ public: 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_disabled_contour = true); + 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; } @@ -249,8 +274,8 @@ protected: 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_z(const GLGizmoBase::UpdateData &data); - void dragging_grabber_xy(const GLGizmoBase::UpdateData &data); + 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; @@ -275,6 +300,8 @@ protected: 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); void render_cut_plane_input_window(CutConnectors &connectors); void init_input_window_data(CutConnectors &connectors); void render_input_window_warning() const; @@ -290,6 +317,8 @@ protected: 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; } @@ -301,10 +330,12 @@ protected: Transform3d get_cut_matrix(const Selection& selection); private: - void set_center(const Vec3d& center, bool update_tbb = false); - bool render_combo(const std::string& label, const std::vector& lines, int& selection_idx); + 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); + 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; @@ -314,16 +345,18 @@ private: 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); + 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); @@ -339,6 +372,13 @@ private: 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/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 1a800eee42..0023a4f63b 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -67,6 +67,7 @@ static const std::map font_icons = { {ImGui::InfoMarkerSmall , "notification_info" }, {ImGui::PlugMarker , "plug" }, {ImGui::DowelMarker , "dowel" }, + {ImGui::SnapMarker , "snap" }, }; static const std::map font_icons_large = { @@ -639,6 +640,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); diff --git a/src/slic3r/GUI/ImGuiWrapper.hpp b/src/slic3r/GUI/ImGuiWrapper.hpp index 26e58d4ad1..92e761a434 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -49,6 +49,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(); @@ -80,6 +85,7 @@ public: 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); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 9c8297b29c..36f284f3a8 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6317,20 +6317,7 @@ void Plater::toggle_layers_editing(bool enable) canvas3D()->force_main_toolbar_left_action(canvas3D()->get_main_toolbar_item_id("layersediting")); } -void Plater::cut(size_t obj_idx, size_t instance_idx, const Transform3d& cut_matrix, ModelObjectCutAttributes attributes) -{ - wxCHECK_RET(obj_idx < p->model.objects.size(), "obj_idx out of bounds"); - auto* object = p->model.objects[obj_idx]; - - wxCHECK_RET(instance_idx < object->instances.size(), "instance_idx out of bounds"); - - wxBusyCursor wait; - - const auto new_objects = object->cut(instance_idx, cut_matrix, attributes); - cut(obj_idx, new_objects); -} - -void Plater::cut(size_t obj_idx, const ModelObjectPtrs& new_objects) +void Plater::apply_cut_object_to_model(size_t obj_idx, const ModelObjectPtrs& new_objects) { model().delete_object(obj_idx); sidebar().obj_list()->delete_object_from_list(obj_idx); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 8eeabae428..bed65d7d13 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -264,8 +264,7 @@ public: void convert_unit(ConversionType conv_type); void toggle_layers_editing(bool enable); - void cut(size_t obj_idx, size_t instance_idx, const Transform3d& cut_matrix, ModelObjectCutAttributes attributes); - void cut(size_t init_obj_idx, const ModelObjectPtrs& cut_objects); + void apply_cut_object_to_model(size_t init_obj_idx, const ModelObjectPtrs& cut_objects); void export_gcode(bool prefer_removable); void export_stl_obj(bool extended = false, bool selection_only = false); diff --git a/src/slic3r/GUI/SceneRaycaster.cpp b/src/slic3r/GUI/SceneRaycaster.cpp index 64493d86b4..08c3407632 100644 --- a/src/slic3r/GUI/SceneRaycaster.cpp +++ b/src/slic3r/GUI/SceneRaycaster.cpp @@ -40,6 +40,7 @@ std::shared_ptr SceneRaycaster::add_raycaster(EType type, in case EType::Bed: { return m_bed.emplace_back(std::make_shared(encode_id(type, id), raycaster, trafo, use_back_faces)); } case EType::Volume: { return m_volumes.emplace_back(std::make_shared(encode_id(type, id), raycaster, trafo, use_back_faces)); } case EType::Gizmo: { return m_gizmos.emplace_back(std::make_shared(encode_id(type, id), raycaster, trafo, use_back_faces)); } + case EType::FallbackGizmo: { return m_fallback_gizmos.emplace_back(std::make_shared(encode_id(type, id), raycaster, trafo, use_back_faces)); } default: { assert(false); return nullptr; } }; } @@ -62,6 +63,7 @@ void SceneRaycaster::remove_raycasters(EType type) case EType::Bed: { m_bed.clear(); break; } case EType::Volume: { m_volumes.clear(); break; } case EType::Gizmo: { m_gizmos.clear(); break; } + case EType::FallbackGizmo: { m_fallback_gizmos.clear(); break; } default: { break; } }; } @@ -86,6 +88,12 @@ void SceneRaycaster::remove_raycaster(std::shared_ptr item) return; } } + for (auto it = m_fallback_gizmos.begin(); it != m_fallback_gizmos.end(); ++it) { + if (*it == item) { + m_fallback_gizmos.erase(it); + return; + } + } } SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Camera& camera, const ClippingPlane* clipping_plane) const @@ -174,6 +182,9 @@ SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Came if (!m_gizmos.empty()) test_raycasters(EType::Gizmo, mouse_pos, camera, ret); + if (!m_fallback_gizmos.empty() && !ret.is_valid()) + test_raycasters(EType::FallbackGizmo, mouse_pos, camera, ret); + if (!m_gizmos_on_top || !ret.is_valid()) { if (camera.is_looking_downward() && !m_bed.empty()) test_raycasters(EType::Bed, mouse_pos, camera, ret); @@ -241,6 +252,14 @@ size_t SceneRaycaster::active_gizmos_count() const { } return count; } +size_t SceneRaycaster::active_fallback_gizmos_count() const { + size_t count = 0; + for (const auto& g : m_fallback_gizmos) { + if (g->is_active()) + ++count; + } + return count; +} #endif // ENABLE_RAYCAST_PICKING_DEBUG std::vector>* SceneRaycaster::get_raycasters(EType type) @@ -251,6 +270,7 @@ std::vector>* SceneRaycaster::get_raycasters case EType::Bed: { ret = &m_bed; break; } case EType::Volume: { ret = &m_volumes; break; } case EType::Gizmo: { ret = &m_gizmos; break; } + case EType::FallbackGizmo: { ret = &m_fallback_gizmos; break; } default: { break; } } assert(ret != nullptr); @@ -265,6 +285,7 @@ const std::vector>* SceneRaycaster::get_rayc case EType::Bed: { ret = &m_bed; break; } case EType::Volume: { ret = &m_volumes; break; } case EType::Gizmo: { ret = &m_gizmos; break; } + case EType::FallbackGizmo: { ret = &m_fallback_gizmos; break; } default: { break; } } assert(ret != nullptr); @@ -278,6 +299,7 @@ int SceneRaycaster::base_id(EType type) case EType::Bed: { return int(EIdBase::Bed); } case EType::Volume: { return int(EIdBase::Volume); } case EType::Gizmo: { return int(EIdBase::Gizmo); } + case EType::FallbackGizmo: { return int(EIdBase::FallbackGizmo); } default: { break; } }; diff --git a/src/slic3r/GUI/SceneRaycaster.hpp b/src/slic3r/GUI/SceneRaycaster.hpp index df44b1701c..8102e20de4 100644 --- a/src/slic3r/GUI/SceneRaycaster.hpp +++ b/src/slic3r/GUI/SceneRaycaster.hpp @@ -42,14 +42,16 @@ public: None, Bed, Volume, - Gizmo + Gizmo, + FallbackGizmo // Is used for gizmo grabbers which will be hit after all grabbers of Gizmo type }; enum class EIdBase { Bed = 0, Volume = 1000, - Gizmo = 1000000 + Gizmo = 1000000, + FallbackGizmo = 2000000 }; struct HitResult @@ -66,6 +68,7 @@ private: std::vector> m_bed; std::vector> m_volumes; std::vector> m_gizmos; + std::vector> m_fallback_gizmos; // When set to true, if checking gizmos returns a valid hit, // the search is not performed on other types @@ -99,9 +102,11 @@ public: size_t beds_count() const { return m_bed.size(); } size_t volumes_count() const { return m_volumes.size(); } size_t gizmos_count() const { return m_gizmos.size(); } + size_t fallback_gizmos_count() const { return m_fallback_gizmos.size(); } size_t active_beds_count() const; size_t active_volumes_count() const; size_t active_gizmos_count() const; + size_t active_fallback_gizmos_count() const; #endif // ENABLE_RAYCAST_PICKING_DEBUG static int decode_id(EType type, int id);