diff --git a/src/imgui/imconfig.h b/src/imgui/imconfig.h index f2c3ef0837..856d29318f 100644 --- a/src/imgui/imconfig.h +++ b/src/imgui/imconfig.h @@ -140,6 +140,8 @@ namespace ImGui const wchar_t CancelButton = 0x14; const wchar_t CancelHoverButton = 0x15; // const wchar_t VarLayerHeightMarker = 0x16; + const wchar_t RevertButton = 0x16; + const wchar_t RevertButton2 = 0x17; const wchar_t RightArrowButton = 0x18; const wchar_t RightArrowHoverButton = 0x19; diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 67450fb116..8d44f3e7cc 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1336,6 +1336,196 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, ModelObjectCutAttr return res; } +ModelObjectPtrs ModelObject::cut(size_t instance, const Vec3d& cut_center, const Vec3d& cut_rotation, 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 = attributes.has(ModelObjectCutAttribute::KeepUpper) ? ModelObject::new_clone(*this) : nullptr; + ModelObject* lower = attributes.has(ModelObjectCutAttribute::KeepLower) ? ModelObject::new_clone(*this) : nullptr; + + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) { + upper->set_model(nullptr); + upper->sla_support_points.clear(); + upper->sla_drain_holes.clear(); + upper->sla_points_status = sla::PointsStatus::NoPoints; + upper->clear_volumes(); + upper->input_file.clear(); + } + + if (attributes.has(ModelObjectCutAttribute::KeepLower)) { + lower->set_model(nullptr); + lower->sla_support_points.clear(); + lower->sla_drain_holes.clear(); + lower->sla_points_status = sla::PointsStatus::NoPoints; + lower->clear_volumes(); + lower->input_file.clear(); + } + + // Because transformations are going to be applied to meshes directly, + // we reset transformation of all instances and volumes, + // except for translation and Z-rotation on instances, which are preserved + // in the transformation matrix and not applied to the mesh transform. + + // const auto instance_matrix = instances[instance]->get_matrix(true); + const auto instance_matrix = Geometry::assemble_transform( + Vec3d::Zero(), // don't apply offset + instances[instance]->get_rotation().cwiseProduct(Vec3d(1.0, 1.0, 1.0)), + instances[instance]->get_scaling_factor(), + instances[instance]->get_mirror() + ); + + const auto cut_matrix = Geometry::assemble_transform( + -cut_center, + Vec3d::Zero(), + Vec3d::Ones(), + Vec3d::Ones() + ); + + const auto invert_cut_matrix = Geometry::assemble_transform( + cut_center, + cut_rotation, + Vec3d::Ones(), + Vec3d::Ones() + ); + + // Displacement (in instance coordinates) to be applied to place the upper parts + Vec3d local_displace = Vec3d::Zero(); + + for (ModelVolume* volume : volumes) { + const auto volume_matrix = volume->get_matrix(); + + volume->supported_facets.reset(); + volume->seam_facets.reset(); + volume->mmu_segmentation_facets.reset(); + + if (!volume->is_model_part()) { + // Modifiers are not cut, but we still need to add the instance transformation + // to the modifier volume transformation to preserve their shape properly. + + volume->set_transformation(Geometry::Transformation(instance_matrix * volume_matrix)); + + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) + upper->add_volume(*volume); + if (attributes.has(ModelObjectCutAttribute::KeepLower)) + lower->add_volume(*volume); + } + else if (!volume->mesh().empty()) { + // Transform the mesh by the combined transformation matrix. + // Flip the triangles in case the composite transformation is left handed. + TriangleMesh mesh(volume->mesh()); + mesh.transform(cut_matrix * instance_matrix * volume_matrix, true); + mesh.rotate(-cut_rotation.z(), Z); + mesh.rotate(-cut_rotation.y(), Y); + mesh.rotate(-cut_rotation.x(), X); + + volume->reset_mesh(); + // Reset volume transformation except for offset + const Vec3d offset = volume->get_offset(); + volume->set_transformation(Geometry::Transformation()); + volume->set_offset(offset); + + // Perform cut + TriangleMesh upper_mesh, lower_mesh; + { + indexed_triangle_set upper_its, lower_its; + cut_mesh(mesh.its, 0.0f, &upper_its, &lower_its); + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) + upper_mesh = TriangleMesh(upper_its); + if (attributes.has(ModelObjectCutAttribute::KeepLower)) + lower_mesh = TriangleMesh(lower_its); + } + + if (attributes.has(ModelObjectCutAttribute::KeepUpper) && !upper_mesh.empty()) { + upper_mesh.transform(invert_cut_matrix); + + ModelVolume* vol = upper->add_volume(upper_mesh); + vol->name = volume->name; + // Don't copy the config's ID. + vol->config.assign_config(volume->config); + assert(vol->config.id().valid()); + assert(vol->config.id() != volume->config.id()); + vol->set_material(volume->material_id(), *volume->material()); + } + if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower_mesh.empty()) { + lower_mesh.transform(invert_cut_matrix); + + ModelVolume* vol = lower->add_volume(lower_mesh); + vol->name = volume->name; + // Don't copy the config's ID. + vol->config.assign_config(volume->config); + assert(vol->config.id().valid()); + assert(vol->config.id() != volume->config.id()); + vol->set_material(volume->material_id(), *volume->material()); + + // Compute the displacement (in instance coordinates) to be applied to place the upper parts + // The upper part displacement is set to half of the lower part bounding box + // this is done in hope at least a part of the upper part will always be visible and draggable + local_displace = lower->full_raw_mesh_bounding_box().size().cwiseProduct(Vec3d(-0.5, -0.5, 0.0)); + } + } + } + + ModelObjectPtrs res; + + if (attributes.has(ModelObjectCutAttribute::KeepUpper) && upper->volumes.size() > 0) { + if (!upper->origin_translation.isApprox(Vec3d::Zero()) && instances[instance]->get_offset().isApprox(Vec3d::Zero())) { + upper->center_around_origin(); + upper->translate_instances(-upper->origin_translation); + upper->origin_translation = Vec3d::Zero(); + } + else { + upper->invalidate_bounding_box(); + upper->center_around_origin(); + } + + // Reset instance transformation except offset and Z-rotation + for (size_t i = 0; i < instances.size(); ++i) { + auto& obj_instance = upper->instances[i]; + const Vec3d offset = obj_instance->get_offset(); + const double rot_z = obj_instance->get_rotation().z(); + const Vec3d displace = Geometry::assemble_transform(Vec3d::Zero(), obj_instance->get_rotation()) * local_displace; + + obj_instance->set_transformation(Geometry::Transformation()); + obj_instance->set_offset(offset + displace); + if (i != instance) + obj_instance->set_rotation(Vec3d(0.0, 0.0, rot_z)); + } + + res.push_back(upper); + } + if (attributes.has(ModelObjectCutAttribute::KeepLower) && lower->volumes.size() > 0) { + if (!lower->origin_translation.isApprox(Vec3d::Zero()) && instances[instance]->get_offset().isApprox(Vec3d::Zero())) { + lower->center_around_origin(); + lower->translate_instances(-lower->origin_translation); + lower->origin_translation = Vec3d::Zero(); + } + else { + lower->invalidate_bounding_box(); + lower->center_around_origin(); + } + + // Reset instance transformation except offset and Z-rotation + for (size_t i = 0; i < instances.size(); ++i) { + auto& obj_instance = lower->instances[i]; + const Vec3d offset = obj_instance->get_offset(); + const double rot_z = obj_instance->get_rotation().z(); + obj_instance->set_transformation(Geometry::Transformation()); + obj_instance->set_offset(offset); + obj_instance->set_rotation(Vec3d(attributes.has(ModelObjectCutAttribute::FlipLower) ? Geometry::deg2rad(180.0) : 0.0, 0.0, i == instance ? 0.0 : rot_z)); + } + + res.push_back(lower); + } + + BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - end"; + + return res; +} + void ModelObject::split(ModelObjectPtrs* new_objects) { for (ModelVolume* volume : this->volumes) { diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 7a1cf206ea..3d933c4702 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -354,6 +354,7 @@ public: size_t facets_count() const; size_t parts_count() const; ModelObjectPtrs cut(size_t instance, coordf_t z, ModelObjectCutAttributes attributes); + ModelObjectPtrs cut(size_t instance, const Vec3d& cut_center, const Vec3d& cut_rotation, 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, diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index f365323abd..1e4b9ea2c5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -34,6 +34,8 @@ void GLGizmoCenterMove::set_center_pos(const Vec3d& centre_pos) set_center(Vec3d(std::clamp(centre_pos.x(), m_min_pos.x(), m_max_pos.x()), std::clamp(centre_pos.y(), m_min_pos.y(), m_max_pos.y()), std::clamp(centre_pos.z(), m_min_pos.z(), m_max_pos.z()))); + + m_center_offset = get_center() - m_bb_center; } std::string GLGizmoCenterMove::get_tooltip() const @@ -55,14 +57,8 @@ std::string GLGizmoCenterMove::get_tooltip() const void GLGizmoCenterMove::on_set_state() { // Reset internal variables on gizmo activation, if bounding box was changed - if (get_state() == On) { - const BoundingBoxf3 box = bounding_box(); - if (m_max_pos != box.max && m_min_pos != box.min) { - m_max_pos = box.max; - m_min_pos = box.min; - set_center_pos(box.center()); - } - } + if (get_state() == On) + update_bb(); } void GLGizmoCenterMove::on_update(const UpdateData& data) @@ -78,12 +74,26 @@ BoundingBoxf3 GLGizmoCenterMove::bounding_box() const const Selection::IndicesList& idxs = selection.get_volume_idxs(); for (unsigned int i : idxs) { const GLVolume* volume = selection.get_volume(i); - if (!volume->is_modifier) + // respect just to the solid parts for FFF and ignore pad and supports for SLA + if (!volume->is_modifier && !volume->is_sla_pad() && !volume->is_sla_support()) ret.merge(volume->transformed_convex_hull_bounding_box()); } return ret; } +bool GLGizmoCenterMove::update_bb() +{ + const BoundingBoxf3 box = bounding_box(); + if (m_max_pos != box.max && m_min_pos != box.min) { + m_max_pos = box.max; + m_min_pos = box.min; + m_bb_center = box.center(); + set_center_pos(m_bb_center + m_center_offset); + return true; + } + + return false; +} GLGizmoCut3D::GLGizmoCut3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) @@ -137,7 +147,7 @@ void GLGizmoCut3D::update_clipper() Vec3d plane_center = m_move_gizmo.get_center(); BoundingBoxf3 box = m_move_gizmo.bounding_box(); - Vec3d min, max = min = plane_center = m_move_gizmo.get_center(); + Vec3d min, max = min = plane_center; min[Z] = box.min.z(); max[Z] = box.max.z(); @@ -155,10 +165,17 @@ void GLGizmoCut3D::update_clipper() m_c->object_clipper()->set_range_and_pos(beg, end, dist); } +void GLGizmoCut3D::update_clipper_on_render() +{ + update_clipper(); + suppress_update_clipper_on_render = true; +} + void GLGizmoCut3D::set_center(const Vec3d& center) { m_move_gizmo.set_center_pos(center); m_rotation_gizmo.set_center(m_move_gizmo.get_center()); + update_clipper(); } void GLGizmoCut3D::render_combo(const std::string& label, const std::vector& lines, size_t& selection_idx) @@ -253,8 +270,11 @@ void GLGizmoCut3D::render_rotation_input(int axis) ImGui::InputDouble(("##rotate_" + m_axis_names[axis]).c_str(), &value, 0.0f, 0.0f, "%.2f", ImGuiInputTextFlags_CharsDecimal); ImGui::SameLine(); - rotation[axis] = Geometry::deg2rad(value); - m_rotation_gizmo.set_rotation(rotation); + if (double val = Geometry::deg2rad(value); val != rotation[axis]) { + rotation[axis] = val; + m_rotation_gizmo.set_rotation(rotation); + update_clipper(); + } } void GLGizmoCut3D::render_connect_type_radio_button(ConnectorType type) @@ -273,6 +293,30 @@ void GLGizmoCut3D::render_connect_mode_radio_button(ConnectorMode mode) m_connector_mode = mode; } +bool GLGizmoCut3D::render_revert_button(const wxString& label) +{ + const ImGuiStyle& style = ImGui::GetStyle(); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, { 1, style.ItemSpacing.y }); + ImGui::SameLine(m_label_width); + + ImGui::PushStyleColor(ImGuiCol_Button, { 0.25f, 0.25f, 0.25f, 0.0f }); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, { 0.4f, 0.4f, 0.4f, 1.0f }); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, { 0.4f, 0.4f, 0.4f, 1.0f }); + + bool revert = m_imgui->button(label); + + ImGui::PopStyleColor(3); + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(into_u8(_L("Revert")).c_str(), ImGui::GetFontSize() * 20.0f); + + ImGui::PopStyleVar(); + + ImGui::SameLine(); + return revert; +} + void GLGizmoCut3D::render_cut_plane() { const BoundingBoxf3 box = m_move_gizmo.bounding_box(); @@ -365,6 +409,8 @@ void GLGizmoCut3D::on_set_state() m_move_gizmo.set_state(m_state); m_rotation_gizmo.set_center(m_move_gizmo.get_center()); m_rotation_gizmo.set_state(m_state); + + suppress_update_clipper_on_render = m_state != On; } void GLGizmoCut3D::on_set_hover_id() @@ -388,7 +434,7 @@ void GLGizmoCut3D::on_disable_grabber(unsigned int id) bool GLGizmoCut3D::on_is_activable() const { - return m_move_gizmo.is_activable(); + return m_rotation_gizmo.is_activable() && m_move_gizmo.is_activable(); } void GLGizmoCut3D::on_start_dragging() @@ -408,11 +454,16 @@ void GLGizmoCut3D::on_update(const UpdateData& data) { m_move_gizmo.update(data); m_rotation_gizmo.update(data); + update_clipper(); } void GLGizmoCut3D::on_render() { - update_clipper(); + if (m_move_gizmo.update_bb()) { + m_rotation_gizmo.set_center(m_move_gizmo.get_center()); + update_clipper_on_render(); + } + render_cut_plane(); if (m_mode == CutMode::cutPlanar) { int move_group_id = m_move_gizmo.get_group_id(); @@ -421,6 +472,9 @@ void GLGizmoCut3D::on_render() if (m_hover_id == -1 || m_hover_id >= move_group_id) m_move_gizmo.render(); } + + if (!suppress_update_clipper_on_render) + update_clipper_on_render(); } void GLGizmoCut3D::on_render_input_window(float x, float y, float bottom_limit) @@ -449,20 +503,23 @@ void GLGizmoCut3D::on_render_input_window(float x, float y, float bottom_limit) render_combo(_u8L("Mode"), m_modes, m_mode); + bool revert_rotation{ false }; + bool revert_move{ false }; + if (m_mode <= CutMode::cutByLine) { ImGui::Separator(); if (m_mode == CutMode::cutPlanar) { ImGui::AlignTextToFramePadding(); m_imgui->text(_L("Move center")); - ImGui::SameLine(m_label_width); + revert_move = render_revert_button(ImGui::RevertButton); for (Axis axis : {X, Y, Z}) render_move_center_input(axis); - m_imgui->text(m_imperial_units ? _L("in") : _L("mm")); + m_imgui->text(m_imperial_units ? _L("in") : _L("mm")); ImGui::AlignTextToFramePadding(); m_imgui->text(_L("Rotation")); - ImGui::SameLine(m_label_width); + revert_rotation = render_revert_button(ImGui::RevertButton2); for (Axis axis : {X, Y, Z}) render_rotation_input(axis); m_imgui->text(_L("°")); @@ -524,7 +581,7 @@ void GLGizmoCut3D::on_render_input_window(float x, float y, float bottom_limit) //////// static bool hide_clipped = true; static bool fill_cut = true; - static float contour_width = 0.; + static float contour_width = 0.2f; m_imgui->checkbox("hide_clipped", hide_clipped); m_imgui->checkbox("fill_cut", fill_cut); m_imgui->slider_float("contour_width", &contour_width, 0.f, 3.f); @@ -535,6 +592,13 @@ void GLGizmoCut3D::on_render_input_window(float x, float y, float bottom_limit) if (cut_clicked && (m_keep_upper || m_keep_lower)) perform_cut(m_parent.get_selection()); + + if (revert_move) + set_center(m_move_gizmo.bounding_box().center()); + if (revert_rotation) { + m_rotation_gizmo.set_rotation(Vec3d::Zero()); + update_clipper(); + } } bool GLGizmoCut3D::can_perform_cut() const @@ -553,8 +617,13 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) const GLVolume* first_glvolume = selection.get_volume(*selection.get_volume_idxs().begin()); const double object_cut_z = m_move_gizmo.get_center().z() - first_glvolume->get_sla_shift_z(); + Vec3d instance_offset = wxGetApp().plater()->model().objects[object_idx]->instances[instance_idx]->get_offset(); + + Vec3d cut_center_offset = m_move_gizmo.get_center() - instance_offset; + cut_center_offset[Z] -= first_glvolume->get_sla_shift_z(); + if (0.0 < object_cut_z && can_perform_cut()) - wxGetApp().plater()->cut(object_idx, instance_idx, object_cut_z, + wxGetApp().plater()->cut(object_idx, instance_idx, cut_center_offset, m_rotation_gizmo.get_rotation(), only_if(m_keep_upper, ModelObjectCutAttribute::KeepUpper) | only_if(m_keep_lower, ModelObjectCutAttribute::KeepLower) | only_if(m_rotate_lower, ModelObjectCutAttribute::FlipLower)); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp index 80b128cbed..fcf024db1c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp @@ -19,8 +19,10 @@ public: static const double Margin; private: - Vec3d m_min_pos{ Vec3d::Zero() }; - Vec3d m_max_pos{ Vec3d::Zero() }; + Vec3d m_min_pos { Vec3d::Zero() }; + Vec3d m_max_pos { Vec3d::Zero() }; + Vec3d m_bb_center { Vec3d::Zero() }; + Vec3d m_center_offset { Vec3d::Zero() }; public: GLGizmoCenterMove(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); @@ -33,6 +35,7 @@ protected: public: void set_center_pos(const Vec3d& center_pos); BoundingBoxf3 bounding_box() const; + bool update_bb(); }; @@ -56,6 +59,7 @@ class GLGizmoCut3D : public GLGizmoBase float m_label_width{ 150.0 }; float m_control_width{ 200.0 }; bool m_imperial_units{ false }; + bool suppress_update_clipper_on_render{false}; enum CutMode { cutPlanar @@ -114,11 +118,12 @@ public: void shift_cut_z(double delta); void update_clipper(); + void update_clipper_on_render(); protected: bool on_init() override; - void on_load(cereal::BinaryInputArchive& ar) override { ar(/*m_cut_z, */m_keep_upper, m_keep_lower, m_rotate_lower); } - void on_save(cereal::BinaryOutputArchive& ar) const override { ar(/*m_cut_z, */m_keep_upper, m_keep_lower, m_rotate_lower); } + void on_load(cereal::BinaryInputArchive& ar) override { ar(m_keep_upper, m_keep_lower, m_rotate_lower); } + void on_save(cereal::BinaryOutputArchive& ar) const override { ar(m_keep_upper, m_keep_lower, m_rotate_lower); } std::string on_get_name() const override; void on_set_state() override; CommonGizmosDataID on_get_requirements() const override; @@ -144,6 +149,7 @@ private: void render_move_center_input(int axis); void render_rotation_input(int axis); void render_connect_mode_radio_button(ConnectorMode mode); + bool render_revert_button(const wxString& label); void render_connect_type_radio_button(ConnectorType type); bool can_perform_cut() const; diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index e70c1111b9..5e154e48a3 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -70,6 +70,8 @@ static const std::map font_icons = { {ImGui::LegendShells , "legend_shells" }, {ImGui::LegendToolMarker , "legend_toolmarker" }, #endif // ENABLE_LEGEND_TOOLBAR_ICONS + {ImGui::RevertButton , "undo" }, + {ImGui::RevertButton2 , "undo" }, }; static const std::map font_icons_large = { diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 32cd327a80..0ff3c294c9 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5854,6 +5854,30 @@ void Plater::cut(size_t obj_idx, size_t instance_idx, coordf_t z, ModelObjectCut selection.add_object((unsigned int)(last_id - i), i == 0); } +void Slic3r::GUI::Plater::cut(size_t obj_idx, size_t instance_idx, const Vec3d& cut_center, const Vec3d& cut_rotation, ModelObjectCutAttributes attributes) +{ + wxCHECK_RET(obj_idx < p->model.objects.size(), "obj_idx out of bounds"); + auto* object = p->model.objects[obj_idx]; + + wxCHECK_RET(instance_idx < object->instances.size(), "instance_idx out of bounds"); + + if (!attributes.has(ModelObjectCutAttribute::KeepUpper) && !attributes.has(ModelObjectCutAttribute::KeepLower)) + return; + + Plater::TakeSnapshot snapshot(this, _L("Cut by Plane")); + wxBusyCursor wait; + + const auto new_objects = object->cut(instance_idx, cut_center, cut_rotation, attributes); + + remove(obj_idx); + p->load_model_objects(new_objects); + + Selection& selection = p->get_selection(); + size_t last_id = p->model.objects.size() - 1; + for (size_t i = 0; i < new_objects.size(); ++i) + selection.add_object((unsigned int)(last_id - i), i == 0); +} + void Plater::export_gcode(bool prefer_removable) { if (p->model.objects.empty()) diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index baa54480c9..a168c32d1b 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -254,6 +254,7 @@ public: void toggle_layers_editing(bool enable); void cut(size_t obj_idx, size_t instance_idx, coordf_t z, ModelObjectCutAttributes attributes); + void cut(size_t obj_idx, size_t instance_idx, const Vec3d& cut_center, const Vec3d& cut_rotation, ModelObjectCutAttributes attributes); void export_gcode(bool prefer_removable); void export_stl_obj(bool extended = false, bool selection_only = false);