diff --git a/resources/icons/hollowing.svg b/resources/icons/hollowing.svg new file mode 100644 index 000000000..65c7592c8 --- /dev/null +++ b/resources/icons/hollowing.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/white/hollowing.svg b/resources/icons/white/hollowing.svg new file mode 100644 index 000000000..65c7592c8 --- /dev/null +++ b/resources/icons/white/hollowing.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 061c5bd50..607f11bc6 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1142,7 +1142,8 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b if (keep_upper) { upper->add_volume(*volume); } if (keep_lower) { lower->add_volume(*volume); } } - else { + else if (! volume->mesh().empty()) { + TriangleMesh upper_mesh, lower_mesh; // Transform the mesh by the combined transformation matrix. @@ -1150,7 +1151,9 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b TriangleMesh mesh(volume->mesh()); mesh.transform(instance_matrix * volume_matrix, true); volume->reset_mesh(); - + + mesh.require_shared_vertices(); + // Perform cut TriangleMeshSlicer tms(&mesh); tms.cut(float(z), &upper_mesh, &lower_mesh); diff --git a/src/libslic3r/OpenVDBUtils.cpp b/src/libslic3r/OpenVDBUtils.cpp index 97a57315b..08c69bdb5 100644 --- a/src/libslic3r/OpenVDBUtils.cpp +++ b/src/libslic3r/OpenVDBUtils.cpp @@ -2,6 +2,9 @@ #include "OpenVDBUtils.hpp" #include #include +#include +#include +#include "MTUtils.hpp" namespace Slic3r { @@ -50,6 +53,11 @@ void Contour3DDataAdapter::getIndexSpacePoint(size_t n, pos = {p.x(), p.y(), p.z()}; } + +// TODO: Do I need to call initialize? Seems to work without it as well but the +// docs say it should be called ones. It does a mutex lock-unlock sequence all +// even if was called previously. + openvdb::FloatGrid::Ptr meshToVolume(const TriangleMesh &mesh, const openvdb::math::Transform &tr, float exteriorBandWidth, @@ -62,11 +70,7 @@ openvdb::FloatGrid::Ptr meshToVolume(const TriangleMesh &mesh, interiorBandWidth, flags); } -// TODO: Do I need to call initialize? Seems to work without it as well but the -// docs say it should be called ones. It does a mutex lock-unlock sequence all -// even if was called previously. - -openvdb::FloatGrid::Ptr meshToVolume(const sla::Contour3D & mesh, +static openvdb::FloatGrid::Ptr meshToVolume(const sla::Contour3D &mesh, const openvdb::math::Transform &tr, float exteriorBandWidth, float interiorBandWidth, @@ -84,10 +88,10 @@ inline Vec3i to_vec3i(const openvdb::Vec3I &v) { return Vec3i{int(v[0]), int(v[1 inline Vec4i to_vec4i(const openvdb::Vec4I &v) { return Vec4i{int(v[0]), int(v[1]), int(v[2]), int(v[3])}; } template -sla::Contour3D _volumeToMesh(const Grid &grid, - double isovalue, - double adaptivity, - bool relaxDisorientedTriangles) +sla::Contour3D __volumeToMesh(const Grid &grid, + double isovalue, + double adaptivity, + bool relaxDisorientedTriangles) { openvdb::initialize(); @@ -102,20 +106,106 @@ sla::Contour3D _volumeToMesh(const Grid &grid, ret.points.reserve(points.size()); ret.faces3.reserve(triangles.size()); ret.faces4.reserve(quads.size()); - + for (auto &v : points) ret.points.emplace_back(to_vec3d(v)); for (auto &v : triangles) ret.faces3.emplace_back(to_vec3i(v)); for (auto &v : quads) ret.faces4.emplace_back(to_vec4i(v)); - + return ret; } -sla::Contour3D volumeToMesh(const openvdb::FloatGrid &grid, - double isovalue, - double adaptivity, - bool relaxDisorientedTriangles) +template inline +Mesh _volumeToMesh(const openvdb::FloatGrid &grid, + double isovalue = 0.0, + double adaptivity = 0.0, + bool relaxDisorientedTriangles = true); + +template<> inline +TriangleMesh _volumeToMesh(const openvdb::FloatGrid &grid, + double isovalue, + double adaptivity, + bool relaxDisorientedTriangles) { - return _volumeToMesh(grid, isovalue, adaptivity, relaxDisorientedTriangles); + return to_triangle_mesh(__volumeToMesh(grid, isovalue, adaptivity, + relaxDisorientedTriangles)); +} + +template<> inline +sla::Contour3D _volumeToMesh(const openvdb::FloatGrid &grid, + double isovalue, + double adaptivity, + bool relaxDisorientedTriangles) +{ + return __volumeToMesh(grid, isovalue, adaptivity, + relaxDisorientedTriangles); +} + +TriangleMesh volumeToMesh(const openvdb::FloatGrid &grid, + double isovalue, + double adaptivity, + bool relaxDisorientedTriangles) +{ + return _volumeToMesh(grid, isovalue, adaptivity, + relaxDisorientedTriangles); +} + +template> +inline void _scale(S s, TriangleMesh &m) { m.scale(float(s)); } + +template> +inline void _scale(S s, sla::Contour3D &m) +{ + for (auto &p : m.points) p *= s; +} + +template +remove_cvref_t _hollowed_interior(Mesh &&mesh, + double min_thickness, + int oversampling, + double smoothing) +{ + using MMesh = remove_cvref_t; + MMesh imesh{std::forward(mesh)}; + + // I can't figure out how to increase the grid resolution through openvdb API + // so the model will be scaled up before conversion and the result scaled + // down. Voxels have a unit size. If I set voxelSize smaller, it scales + // the whole geometry down, and doesn't increase the number of voxels. + auto scale = double(oversampling); + + _scale(scale, imesh); + + double offset = scale * min_thickness; + float range = float(std::max(2 * offset, scale)); + auto gridptr = meshToVolume(imesh, {}, 0.1f * float(offset), range); + + assert(gridptr); + + if (!gridptr) { + BOOST_LOG_TRIVIAL(error) << "Returned OpenVDB grid is NULL"; + return MMesh{}; + } + + // Filtering: + int width = int(smoothing * scale); + int count = 1; + openvdb::tools::Filter{*gridptr}.gaussian(width, count); + + double iso_surface = -offset; + double adaptivity = 0.; + auto omesh = _volumeToMesh(*gridptr, iso_surface, adaptivity); + + _scale(1. / scale, omesh); + + return omesh; +} + +TriangleMesh hollowed_interior(const TriangleMesh &mesh, + double min_thickness, + int oversampling, + double smoothing) +{ + return _hollowed_interior(mesh, min_thickness, oversampling, smoothing); } } // namespace Slic3r diff --git a/src/libslic3r/OpenVDBUtils.hpp b/src/libslic3r/OpenVDBUtils.hpp index eae1e051d..8e55300bd 100644 --- a/src/libslic3r/OpenVDBUtils.hpp +++ b/src/libslic3r/OpenVDBUtils.hpp @@ -13,16 +13,21 @@ openvdb::FloatGrid::Ptr meshToVolume(const TriangleMesh & mesh, float interiorBandWidth = 3.0f, int flags = 0); -openvdb::FloatGrid::Ptr meshToVolume(const sla::Contour3D & mesh, - const openvdb::math::Transform &tr = {}, - float exteriorBandWidth = 3.0f, - float interiorBandWidth = 3.0f, - int flags = 0); +TriangleMesh volumeToMesh(const openvdb::FloatGrid &grid, + double isovalue = 0.0, + double adaptivity = 0.0, + bool relaxDisorientedTriangles = true); -sla::Contour3D volumeToMesh(const openvdb::FloatGrid &grid, - double isovalue = 0.0, - double adaptivity = 0.0, - bool relaxDisorientedTriangles = true); +sla::Contour3D hollowed_interior(sla::Contour3D&& mesh, double min_thickness); +sla::Contour3D hollowed_interior(sla::Contour3D& mesh, double min_thickness); + +// Generate an interior for any solid geometry maintaining a given minimum +// wall thickness. The returned mesh has triangles with normals facing inside +// the mesh so the result can be directly merged with the input to finish the +// hollowing. +// TODO: The thicknes is not strictly maintained due to the used gaussian filter +TriangleMesh hollowed_interior(const TriangleMesh &mesh, double min_thickness, + int oversampling = 3, double smoothing = 0.5); } // namespace Slic3r diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index e639a6734..4cfd81bc5 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2830,6 +2830,22 @@ void PrintConfigDef::init_sla_params() def->min = 0; def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(0.3)); + + def = this->add("hollowing_enable", coBool); + def->label = L("Enable hollowing"); + def->category = L("Hollowing"); + def->tooltip = L("Hollow out a model to have an empty interior"); + def->mode = comSimple; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("hollowing_min_thickness", coFloat); + def->label = L("Hollowing thickness"); + def->category = L("Hollowing"); + def->tooltip = L("Minimum wall thickness of a hollowed model."); + def->sidetext = L("mm"); + def->min = 1; + def->mode = comSimple; + def->set_default_value(new ConfigOptionFloat(4)); } void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &value) diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 4b007fc51..6bcc3f9ad 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1014,7 +1014,7 @@ public: ConfigOptionFloat support_base_height /*= 1.0*/; // The minimum distance of the pillar base from the model in mm. - ConfigOptionFloat support_base_safety_distance; /*= 1.0*/; + ConfigOptionFloat support_base_safety_distance; /*= 1.0*/ // The default angle for connecting support sticks and junctions. ConfigOptionFloat support_critical_angle /*= 45*/; @@ -1059,7 +1059,7 @@ public: // ///////////////////////////////////////////////////////////////////////// // Zero elevation mode parameters: - // - The object pad will be derived from the the model geometry. + // - The object pad will be derived from the model geometry. // - There will be a gap between the object pad and the generated pad // according to the support_base_safety_distance parameter. // - The two pads will be connected with tiny connector sticks @@ -1081,6 +1081,22 @@ public: // How much should the tiny connectors penetrate into the model body ConfigOptionFloat pad_object_connector_penetration; + + // ///////////////////////////////////////////////////////////////////////// + // Model hollowing parameters: + // - Models can be hollowed out as part of the SLA print process + // - Thickness of the hollowed model walls can be adjusted + // - + // - Additional holes will be drilled into the hollow model to allow for + // - resin removal. + // ///////////////////////////////////////////////////////////////////////// + + ConfigOptionBool hollowing_enable; + + // The minimum thickness of the model walls to maintain. Note that the + // resulting walls may be thicker due to smoothing out fine cavities where + // resin could stuck. + ConfigOptionFloat hollowing_min_thickness; protected: void initialize(StaticCacheBase &cache, const char *base_ptr) @@ -1118,6 +1134,8 @@ protected: OPT_PTR(pad_object_connector_stride); OPT_PTR(pad_object_connector_width); OPT_PTR(pad_object_connector_penetration); + OPT_PTR(hollowing_enable); + OPT_PTR(hollowing_min_thickness); } }; diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index 8c03853e3..0f5cb20b8 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -3,6 +3,7 @@ #include "SLA/SLAPad.hpp" #include "SLA/SLAAutoSupports.hpp" #include "ClipperUtils.hpp" +#include "OpenVDBUtils.hpp" #include "Geometry.hpp" #include "MTUtils.hpp" @@ -47,6 +48,14 @@ public: } }; +class SLAPrintObject::HollowingData +{ +public: + + TriangleMesh interior; + // std::vector +}; + namespace { // should add up to 100 (%) @@ -752,6 +761,28 @@ void SLAPrint::process() // the coefficient that multiplies the per object status values which // are set up for <0, 100>. They need to be scaled into the whole process const double ostepd = (max_objstatus - min_objstatus) / (objcount * 100.0); + + auto hollow_model = [](SLAPrintObject &po) { + + if (!po.m_config.hollowing_enable.getBool()) { + BOOST_LOG_TRIVIAL(info) << "Skipping hollowing step!"; + po.m_hollowing_data.reset(); + return; + } else { + BOOST_LOG_TRIVIAL(info) << "Performing hollowing step!"; + } + + if (!po.m_hollowing_data) + po.m_hollowing_data.reset(new SLAPrintObject::HollowingData()); + + double thickness = po.m_config.hollowing_min_thickness.getFloat(); + + po.m_hollowing_data->interior = + hollowed_interior(po.transformed_mesh(), thickness, 4, 0.5); + + if (po.m_hollowing_data->interior.empty()) + BOOST_LOG_TRIVIAL(warning) << "Hollowed interior is empty!"; + }; // The slicing will be performed on an imaginary 1D grid which starts from // the bottom of the bounding box created around the supported model. So @@ -765,7 +796,20 @@ void SLAPrint::process() // Slicing the model object. This method is oversimplified and needs to // be compared with the fff slicing algorithm for verification auto slice_model = [this, ilhs, ilh](SLAPrintObject& po) { - const TriangleMesh& mesh = po.transformed_mesh(); + + TriangleMesh hollowed_mesh; + + bool is_hollowing = po.m_config.hollowing_enable.getBool() && + po.m_hollowing_data; + + if (is_hollowing) { + hollowed_mesh = po.transformed_mesh(); + hollowed_mesh.merge(po.m_hollowing_data->interior); + hollowed_mesh.require_shared_vertices(); + } + + const TriangleMesh &mesh = is_hollowing ? hollowed_mesh : + po.transformed_mesh(); // We need to prepare the slice index... @@ -1465,12 +1509,12 @@ void SLAPrint::process() slaposFn pobj_program[] = { - [](SLAPrintObject&){}, slice_model, [](SLAPrintObject&){}, support_points, support_tree, generate_pad, slice_supports + hollow_model, slice_model, [](SLAPrintObject&){}, support_points, support_tree, generate_pad, slice_supports }; // We want to first process all objects... std::vector level1_obj_steps = { - slaposObjectSlice, slaposSupportPoints, slaposSupportTree, slaposPad + slaposHollowing, slaposObjectSlice, slaposSupportPoints, slaposSupportTree, slaposPad }; // and then slice all supports to allow preview to be displayed ASAP @@ -1707,7 +1751,11 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vector steps; bool invalidated = false; for (const t_config_option_key &opt_key : opt_keys) { - if ( opt_key == "layer_height" + if ( opt_key == "hollowing_enable" + || opt_key == "hollowing_min_thickness") { + steps.emplace_back(slaposHollowing); + } else if ( + opt_key == "layer_height" || opt_key == "faded_layers" || opt_key == "pad_enable" || opt_key == "pad_wall_thickness" diff --git a/src/libslic3r/SLAPrint.hpp b/src/libslic3r/SLAPrint.hpp index 38e373775..1b8b126e2 100644 --- a/src/libslic3r/SLAPrint.hpp +++ b/src/libslic3r/SLAPrint.hpp @@ -74,8 +74,11 @@ public: // Support mesh is only valid if this->is_step_done(slaposSupportTree) is true. const TriangleMesh& support_mesh() const; // Get a pad mesh centered around origin in XY, and with zero rotation around Z applied. - // Support mesh is only valid if this->is_step_done(slaposBasePool) is true. + // Support mesh is only valid if this->is_step_done(slaposPad) is true. const TriangleMesh& pad_mesh() const; + + // Ready after this->is_step_done(slaposHollowing) is true + const TriangleMesh& hollowed_interior_mesh() const; // This will return the transformed mesh which is cached const TriangleMesh& transformed_mesh() const; @@ -288,9 +291,12 @@ private: // Caching the transformed (m_trafo) raw mesh of the object mutable CachedObject m_transformed_rmesh; - + class SupportData; std::unique_ptr m_supportdata; + + class HollowingData; + std::unique_ptr m_hollowing_data; }; using PrintObjects = std::vector; diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index a88980a8d..45d55c0b9 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -98,6 +98,7 @@ ObjectList::ObjectList(wxWindow* parent) : // ptSLA CATEGORY_ICON[L("Supports")] = create_scaled_bitmap(nullptr, "support"/*"sla_supports"*/); CATEGORY_ICON[L("Pad")] = create_scaled_bitmap(nullptr, "pad"); + CATEGORY_ICON[L("Hollowing")] = create_scaled_bitmap(nullptr, "hollowing"); } // create control diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp index 5b6620ef6..34173366d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp @@ -597,16 +597,20 @@ void GLGizmoHollow::on_update(const UpdateData& data) void GLGizmoHollow::hollow_mesh(float offset, float adaptibility) { - Slic3r::sla::Contour3D imesh{*m_mesh}; - auto ptr = meshToVolume(imesh, {}); - sla::Contour3D omesh = volumeToMesh(*ptr, -offset, adaptibility, true); +// Slic3r::sla::Contour3D imesh{*m_mesh}; +// auto ptr = meshToVolume(imesh, {}); +// sla::Contour3D omesh = volumeToMesh(*ptr, -offset, adaptibility, true); - if (omesh.empty()) +// if (omesh.empty()) +// return; + +// imesh.merge(omesh); + TriangleMesh cavity = hollowed_interior(*m_mesh, double(offset), int(adaptibility)); + if (cavity.empty()) return; - - imesh.merge(omesh); - m_cavity_mesh.reset(new TriangleMesh); - *m_cavity_mesh = sla::to_triangle_mesh(imesh); + + m_cavity_mesh.reset(new TriangleMesh(cavity)); + m_cavity_mesh->merge(*m_mesh); m_cavity_mesh.get()->require_shared_vertices(); m_mesh_raycaster.reset(new MeshRaycaster(*m_cavity_mesh.get())); m_object_clipper.reset(); @@ -616,7 +620,7 @@ void GLGizmoHollow::hollow_mesh(float offset, float adaptibility) m_volume_with_cavity->indexed_vertex_array.load_mesh(*m_cavity_mesh.get()); m_volume_with_cavity->finalize_geometry(true); m_volume_with_cavity->set_volume_transformation(m_model_object->volumes.front()->get_transformation()); - m_volume_with_cavity->set_instance_transformation(m_model_object->instances[m_active_instance]->get_transformation()); + m_volume_with_cavity->set_instance_transformation(m_model_object->instances[size_t(m_active_instance)]->get_transformation()); m_parent.toggle_model_objects_visibility(false, m_model_object, m_active_instance); } diff --git a/src/slic3r/GUI/Preset.cpp b/src/slic3r/GUI/Preset.cpp index a360aaf1a..edbb5fb75 100644 --- a/src/slic3r/GUI/Preset.cpp +++ b/src/slic3r/GUI/Preset.cpp @@ -496,6 +496,8 @@ const std::vector& Preset::sla_print_options() "pad_object_connector_stride", "pad_object_connector_width", "pad_object_connector_penetration", + "hollowing_enable", + "hollowing_min_thickness", "output_filename_format", "default_sla_print_profile", "compatible_printers", diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c2a258e69..22c4bfb9b 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3563,6 +3563,11 @@ void TabSLAPrint::build() optgroup->append_single_option_line("pad_object_connector_stride"); optgroup->append_single_option_line("pad_object_connector_width"); optgroup->append_single_option_line("pad_object_connector_penetration"); + + page = add_options_page(_(L("Hollowing")), "hollowing"); + optgroup = page->new_optgroup(_(L("Hollowing"))); + optgroup->append_single_option_line("hollowing_enable"); + optgroup->append_single_option_line("hollowing_min_thickness"); page = add_options_page(_(L("Advanced")), "wrench"); optgroup = page->new_optgroup(_(L("Slicing"))); diff --git a/tests/libslic3r/test_hollowing.cpp b/tests/libslic3r/test_hollowing.cpp index 8f57828a1..b463fd863 100644 --- a/tests/libslic3r/test_hollowing.cpp +++ b/tests/libslic3r/test_hollowing.cpp @@ -22,40 +22,38 @@ static Slic3r::TriangleMesh load_model(const std::string &obj_filename) return mesh; } -static Slic3r::TriangleMesh hollowed_interior(const Slic3r::TriangleMesh &mesh, - double min_thickness) -{ - Slic3r::sla::Contour3D imesh = Slic3r::sla::Contour3D{mesh}; +//static Slic3r::TriangleMesh hollowed_interior(const Slic3r::TriangleMesh &mesh, +// double min_thickness) +//{ +// Slic3r::sla::Contour3D imesh = Slic3r::sla::Contour3D{mesh}; - double scale = std::max(1.0, 3. / min_thickness); - double offset = scale * min_thickness; - float range = float(std::max(2 * offset, scale)); +// double scale = std::max(1.0, 3. / min_thickness); +// double offset = scale * min_thickness; +// float range = float(std::max(2 * offset, scale)); - for (auto &p : imesh.points) p *= scale; - auto ptr = Slic3r::meshToVolume(imesh, {}, 0.1f * float(offset), range); +// for (auto &p : imesh.points) p *= scale; +// auto ptr = Slic3r::meshToVolume(imesh, {}, 0.1f * float(offset), range); - REQUIRE(ptr); +// REQUIRE(ptr); - openvdb::tools::Filter{*ptr}.gaussian(int(scale), 1); +// openvdb::tools::Filter{*ptr}.gaussian(int(scale), 1); - double iso_surface = -offset; - double adaptivity = 0.; - Slic3r::sla::Contour3D omesh = Slic3r::volumeToMesh(*ptr, iso_surface, adaptivity); +// double iso_surface = -offset; +// double adaptivity = 0.; +// Slic3r::sla::Contour3D omesh = Slic3r::volumeToMesh(*ptr, iso_surface, adaptivity); - REQUIRE(!omesh.empty()); +// for (auto &p : omesh.points) p /= scale; - for (auto &p : omesh.points) p /= scale; - - return to_triangle_mesh(omesh); -} +// return to_triangle_mesh(omesh); +//} TEST_CASE("Negative 3D offset should produce smaller object.", "[Hollowing]") { - Slic3r::TriangleMesh in_mesh = load_model("20mm_cube.obj"); + Slic3r::TriangleMesh in_mesh = load_model("zaba.obj"); Benchmark bench; bench.start(); - Slic3r::TriangleMesh out_mesh = hollowed_interior(in_mesh, 2); + Slic3r::TriangleMesh out_mesh = hollowed_interior(in_mesh, 0.5); bench.stop();