diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index c3ab9c3a17..a880518ca3 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -90,6 +90,10 @@ set(SLIC3R_SOURCES ExtrusionRole.hpp ExtrusionSimulator.cpp ExtrusionSimulator.hpp + Feature/Interlocking/InterlockingGenerator.cpp + Feature/Interlocking/InterlockingGenerator.hpp + Feature/Interlocking/VoxelUtils.cpp + Feature/Interlocking/VoxelUtils.hpp FileParserError.hpp Feature/FuzzySkin/FuzzySkin.cpp Feature/FuzzySkin/FuzzySkin.hpp diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index 0c36cc3f11..a5ff5a2591 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -829,6 +829,11 @@ Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons &subject, const Slic3r::Pol Slic3r::ExPolygons union_ex(const Slic3r::Surfaces &subject) { return PolyTreeToExPolygons(clipper_do_polytree(ClipperLib::ctUnion, ClipperUtils::SurfacesProvider(subject), ClipperUtils::EmptyPathsProvider(), ClipperLib::pftNonZero)); } +Slic3r::ExPolygons xor_ex(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset) + { return _clipper_ex(ClipperLib::ctXor, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonProvider(clip), do_safety_offset); } +Slic3r::ExPolygons xor_ex(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) + { return _clipper_ex(ClipperLib::ctXor, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } + template Polylines _clipper_pl_open(ClipperLib::ClipType clipType, PathsProvider1 &&subject, PathsProvider2 &&clip) { diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index 6a01adef1f..d38bd2aa83 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -544,6 +544,9 @@ Slic3r::Polygons union_pt_chained_outside_in(const Slic3r::Polygons &subject); // However, performing the union operation incrementally can be significantly faster in such cases. Slic3r::Polygons union_parallel_reduce(const Slic3r::Polygons &subject); +Slic3r::ExPolygons xor_ex(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); +Slic3r::ExPolygons xor_ex(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); + ClipperLib::PolyNodes order_nodes(const ClipperLib::PolyNodes &nodes); // Implementing generalized loop (foreach) over a list of nodes which can be diff --git a/src/libslic3r/Feature/Interlocking/InterlockingGenerator.cpp b/src/libslic3r/Feature/Interlocking/InterlockingGenerator.cpp new file mode 100644 index 0000000000..a6290337e0 --- /dev/null +++ b/src/libslic3r/Feature/Interlocking/InterlockingGenerator.cpp @@ -0,0 +1,334 @@ +// Copyright (c) 2023 UltiMaker +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "InterlockingGenerator.hpp" + +#include "libslic3r/ClipperUtils.hpp" + +namespace std { +template<> struct hash +{ + size_t operator()(const Slic3r::GridPoint3& pp) const noexcept + { + static int prime = 31; + int result = 89; + result = static_cast(result * prime + pp.x()); + result = static_cast(result * prime + pp.y()); + result = static_cast(result * prime + pp.z()); + return static_cast(result); + } +}; +} // namespace std + + +namespace Slic3r { + +void InterlockingGenerator::generate_interlocking_structure(PrintObject &print_object) +{ + const PrintObjectConfig &config = print_object.config(); + if (!config.interlocking_beam) { + return; + } + + const std::vector &nozzle_diameters = print_object.print()->config().nozzle_diameter.values; + double min_nozzle_diameter = *std::min_element(nozzle_diameters.begin(), nozzle_diameters.end()); + const float rotation = Geometry::deg2rad(config.interlocking_orientation.value); + const coord_t beam_layer_count = config.interlocking_beam_layer_count; + const int interface_depth = config.interlocking_depth; + const int boundary_avoidance = config.interlocking_boundary_avoidance; + const coord_t beam_width = scaled(std::max(min_nozzle_diameter, config.interlocking_beam_width.value)); + + const DilationKernel interface_dilation(GridPoint3(interface_depth, interface_depth, interface_depth), DilationKernel::Type::PRISM); + + const bool air_filtering = boundary_avoidance > 0; + const DilationKernel air_dilation(GridPoint3(boundary_avoidance, boundary_avoidance, boundary_avoidance), DilationKernel::Type::PRISM); + + const coord_t cell_width = beam_width + beam_width; + const Vec3crd cell_size(cell_width, cell_width, 2 * beam_layer_count); + + for (size_t region_a_index = 0; region_a_index < print_object.num_printing_regions(); region_a_index++) { + const PrintRegion& region_a = print_object.printing_region(region_a_index); + const auto extruder_nr_a = region_a.extruder(FlowRole::frExternalPerimeter); + + for (size_t region_b_index = region_a_index + 1; region_b_index < print_object.num_printing_regions(); region_b_index++) { + const PrintRegion& region_b = print_object.printing_region(region_b_index); + const auto extruder_nr_b = region_b.extruder(FlowRole::frExternalPerimeter); + if (extruder_nr_a == extruder_nr_b) { + continue; + } + + InterlockingGenerator gen(print_object, region_a_index, region_b_index, beam_width, boundary_avoidance, rotation, cell_size, beam_layer_count, + interface_dilation, air_dilation, air_filtering); + gen.generateInterlockingStructure(); + } + } +} + +std::pair InterlockingGenerator::growBorderAreasPerpendicular(const ExPolygons& a, const ExPolygons& b, const coord_t& detect) const +{ + const coord_t min_line = + std::min(print_object.printing_region(region_a_index).flow(print_object, frExternalPerimeter, 0.1).scaled_width(), + print_object.printing_region(region_b_index).flow(print_object, frExternalPerimeter, 0.1).scaled_width()); + + const ExPolygons total_shrunk = offset_ex(union_ex(offset_ex(a, min_line), offset_ex(b, min_line)), 2 * -min_line); + + ExPolygons from_border_a = diff_ex(a, total_shrunk); + ExPolygons from_border_b = diff_ex(b, total_shrunk); + + ExPolygons temp_a, temp_b; + for (coord_t i = 0; i < (detect / min_line) + 2; ++i) { + temp_a = offset_ex(from_border_a, min_line); + temp_b = offset_ex(from_border_b, min_line); + from_border_a = diff_ex(temp_a, temp_b); + from_border_b = diff_ex(temp_b, temp_a); + } + + return {from_border_a, from_border_b}; +} + +void InterlockingGenerator::handleThinAreas(const std::unordered_set& has_all_meshes) const +{ + const coord_t number_of_beams_detect = boundary_avoidance; + const coord_t number_of_beams_expand = boundary_avoidance - 1; + constexpr coord_t rounding_errors = 5; + + const coord_t max_beam_width = beam_width; + const coord_t detect = (max_beam_width * number_of_beams_detect) + rounding_errors; + const coord_t expand = (max_beam_width * number_of_beams_expand) + rounding_errors; + const coord_t close_gaps = + std::min(print_object.printing_region(region_a_index).flow(print_object, frExternalPerimeter, 0.1).scaled_width(), + print_object.printing_region(region_b_index).flow(print_object, frExternalPerimeter, 0.1).scaled_width()) / 4; + + // Make an inclusionary polygon, to only actually handle thin areas near actual microstructures (so not in skin for example). + std::vector near_interlock_per_layer; + near_interlock_per_layer.assign(print_object.layer_count(), Polygons()); + for (const auto& cell : has_all_meshes) { + const auto bottom_corner = vu.toLowerCorner(cell); + for (coord_t layer_nr = bottom_corner.z(); + layer_nr < bottom_corner.z() + cell_size.z() && layer_nr < static_cast(near_interlock_per_layer.size()); ++layer_nr) { + near_interlock_per_layer[static_cast(layer_nr)].push_back(vu.toPolygon(cell)); + } + } + for (auto& near_interlock : near_interlock_per_layer) { + near_interlock = offset(union_(closing(near_interlock, rounding_errors)), detect); + polygons_rotate(near_interlock, rotation); + } + + // Only alter layers when they are present in both meshes, zip should take care if that. + for (size_t layer_nr = 0; layer_nr < print_object.layer_count(); layer_nr++){ + auto layer = print_object.get_layer(layer_nr); + ExPolygons polys_a = to_expolygons(layer->get_region(region_a_index)->slices().surfaces); + ExPolygons polys_b = to_expolygons(layer->get_region(region_b_index)->slices().surfaces); + + const auto [from_border_a, from_border_b] = growBorderAreasPerpendicular(polys_a, polys_b, detect); + + // Get the areas of each mesh that are _not_ thin (large), by performing a morphological open. + const ExPolygons large_a = opening_ex(polys_a, detect); + const ExPolygons large_b = opening_ex(polys_b, detect); + + // Derive the area that the thin areas need to expand into (so the added areas to the thin strips) from the information we already have. + const ExPolygons thin_expansion_a = + offset_ex(intersection_ex(intersection_ex(intersection_ex(large_b, offset_ex(diff_ex(polys_a, large_a), expand)), + near_interlock_per_layer[layer_nr]), + from_border_a), + rounding_errors); + const ExPolygons thin_expansion_b = + offset_ex(intersection_ex(intersection_ex(intersection_ex(large_a, offset_ex(diff_ex(polys_b, large_b), expand)), + near_interlock_per_layer[layer_nr]), + from_border_b), + rounding_errors); + + // Expanded thin areas of the opposing polygon should 'eat into' the larger areas of the polygon, + // and conversely, add the expansions to their own thin areas. + layer->get_region(region_a_index)->m_slices.set(closing_ex(diff_ex(union_ex(polys_a, thin_expansion_a), thin_expansion_b), close_gaps), stInternal); + layer->get_region(region_b_index)->m_slices.set(closing_ex(diff_ex(union_ex(polys_b, thin_expansion_b), thin_expansion_a), close_gaps), stInternal); + } +} + +void InterlockingGenerator::generateInterlockingStructure() const +{ + std::vector> voxels_per_mesh = getShellVoxels(interface_dilation); + + std::unordered_set& has_any_mesh = voxels_per_mesh[0]; + std::unordered_set& has_all_meshes = voxels_per_mesh[1]; + has_any_mesh.merge(has_all_meshes); // perform union and intersection simultaneously. Cannibalizes voxels_per_mesh + + if (has_all_meshes.empty()) { + return; + } + + const std::vector layer_regions = computeUnionedVolumeRegions(); + + if (air_filtering) { + std::unordered_set air_cells; + addBoundaryCells(layer_regions, air_dilation, air_cells); + + for (const GridPoint3& p : air_cells) { + has_all_meshes.erase(p); + } + + handleThinAreas(has_all_meshes); + } + + applyMicrostructureToOutlines(has_all_meshes, layer_regions); +} + +std::vector> InterlockingGenerator::getShellVoxels(const DilationKernel& kernel) const +{ + std::vector> voxels_per_mesh(2); + + // mark all cells which contain some boundary + for (size_t region_idx = 0; region_idx < 2; region_idx++) + { + const size_t region = (region_idx == 0) ? region_a_index : region_b_index; + std::unordered_set& mesh_voxels = voxels_per_mesh[region_idx]; + + std::vector rotated_polygons_per_layer(print_object.layer_count()); + for (size_t layer_nr = 0; layer_nr < print_object.layer_count(); layer_nr++) + { + auto layer = print_object.get_layer(layer_nr); + rotated_polygons_per_layer[layer_nr] = to_expolygons(layer->get_region(region)->slices().surfaces); + expolygons_rotate(rotated_polygons_per_layer[layer_nr], rotation); + } + + addBoundaryCells(rotated_polygons_per_layer, kernel, mesh_voxels); + } + + return voxels_per_mesh; +} + +void InterlockingGenerator::addBoundaryCells(const std::vector& layers, + const DilationKernel& kernel, + std::unordered_set& cells) const +{ + auto voxel_emplacer = [&cells](GridPoint3 p) { + if (p.z() < 0) { + return true; + } + cells.emplace(p); + return true; + }; + + for (size_t layer_nr = 0; layer_nr < layers.size(); layer_nr++) { + const coord_t z = static_cast(layer_nr); + vu.walkDilatedPolygons(layers[layer_nr], z, kernel, voxel_emplacer); + ExPolygons skin = layers[layer_nr]; + if (layer_nr > 0) { + skin = xor_ex(skin, layers[layer_nr - 1]); + } + skin = opening_ex(skin, cell_size.x() / 2.f); // remove superfluous small areas, which would anyway be included because of walkPolygons + vu.walkDilatedAreas(skin, z, kernel, voxel_emplacer); + } +} + +std::vector InterlockingGenerator::computeUnionedVolumeRegions() const +{ + const size_t max_layer_count = print_object.layer_count() + + 1; // introduce ghost layer on top for correct skin computation of topmost layer. + std::vector layer_regions(max_layer_count); + + for (size_t layer_nr = 0; layer_nr < max_layer_count - 1; layer_nr++) { + auto& layer_region = layer_regions[static_cast(layer_nr)]; + for (size_t region_idx : {region_a_index, region_b_index}) { + auto layer = print_object.get_layer(layer_nr); + expolygons_append(layer_region, to_expolygons(layer->get_region(region_idx)->slices().surfaces)); + } + layer_region = closing_ex(layer_region, ignored_gap_); // Morphological close to merge meshes into single volume + expolygons_rotate(layer_region, rotation); + } + return layer_regions; +} + +std::vector> InterlockingGenerator::generateMicrostructure() const +{ + std::vector> cell_area_per_mesh_per_layer; + cell_area_per_mesh_per_layer.resize(2); + cell_area_per_mesh_per_layer[0].resize(2); + const coord_t beam_w_sum = beam_width + beam_width; + const coord_t middle = coord_t(int64_t(cell_size.x()) * int64_t(beam_width) / beam_w_sum); + const coord_t width[2] = {middle, cell_size.x() - middle}; + for (size_t mesh_idx : {0ul, 1ul}) { + Point offset(mesh_idx ? middle : 0, 0); + Point area_size(width[mesh_idx], cell_size.y()); + + Polygon poly; + poly.append(offset); + poly.append(offset + Point(area_size.x(), 0)); + poly.append(offset + area_size); + poly.append(offset + Point(0, area_size.y())); + cell_area_per_mesh_per_layer[0][mesh_idx].emplace_back(poly); + } + cell_area_per_mesh_per_layer[1] = cell_area_per_mesh_per_layer[0]; + for (ExPolygons& polys : cell_area_per_mesh_per_layer[1]) { + for (ExPolygon& poly : polys) { + for (Point& p : poly.contour) { + std::swap(p.x(), p.y()); + } + } + } + return cell_area_per_mesh_per_layer; +} + +void InterlockingGenerator::applyMicrostructureToOutlines(const std::unordered_set& cells, + const std::vector& layer_regions) const +{ + std::vector> cell_area_per_mesh_per_layer = generateMicrostructure(); + + const float unapply_rotation = -rotation; + const size_t max_layer_count = print_object.layer_count(); + + std::vector structure_per_layer[2]; // for each mesh the structure on each layer + + // Every `beam_layer_count` number of layers are combined to an interlocking beam layer + // to store these we need ceil(max_layer_count / beam_layer_count) of these layers + // the formula is rewritten as (max_layer_count + beam_layer_count - 1) / beam_layer_count, so it works for integer division + size_t num_interlocking_layers = (max_layer_count + static_cast(beam_layer_count) - 1ul) / + static_cast(beam_layer_count); + structure_per_layer[0].resize(num_interlocking_layers); + structure_per_layer[1].resize(num_interlocking_layers); + + // Only compute cell structure for half the layers, because since our beams are two layers high, every odd layer of the structure will + // be the same as the layer below. + for (const GridPoint3& grid_loc : cells) { + Vec3crd bottom_corner = vu.toLowerCorner(grid_loc); + for (size_t mesh_idx = 0; mesh_idx < 2; mesh_idx++) { + for (size_t layer_nr = bottom_corner.z(); layer_nr < bottom_corner.z() + cell_size.z() && layer_nr < max_layer_count; + layer_nr += beam_layer_count) { + ExPolygons areas_here = cell_area_per_mesh_per_layer[static_cast(layer_nr / beam_layer_count) % + cell_area_per_mesh_per_layer.size()][mesh_idx]; + for (auto & here : areas_here) { + here.translate(bottom_corner.x(), bottom_corner.y()); + } + expolygons_append(structure_per_layer[mesh_idx][static_cast(layer_nr / beam_layer_count)], areas_here); + } + } + } + + for (size_t mesh_idx = 0; mesh_idx < 2; mesh_idx++) { + for (size_t layer_nr = 0; layer_nr < structure_per_layer[mesh_idx].size(); layer_nr++) { + ExPolygons& layer_structure = structure_per_layer[mesh_idx][layer_nr]; + layer_structure = union_ex(layer_structure); + expolygons_rotate(layer_structure, unapply_rotation); + } + } + + for (size_t region_idx = 0; region_idx < 2; region_idx++) { + const size_t region = (region_idx == 0) ? region_a_index : region_b_index; + for (size_t layer_nr = 0; layer_nr < max_layer_count; layer_nr++) { + ExPolygons layer_outlines = layer_regions[layer_nr]; + expolygons_rotate(layer_outlines, unapply_rotation); + + const ExPolygons areas_here = intersection_ex(structure_per_layer[region_idx][layer_nr / static_cast(beam_layer_count)], layer_outlines); + const ExPolygons& areas_other = structure_per_layer[!region_idx][layer_nr / static_cast(beam_layer_count)]; + + auto layer = print_object.get_layer(layer_nr); + auto& slices = layer->get_region(region)->m_slices; + ExPolygons polys = to_expolygons(slices.surfaces); + slices.set(union_ex(diff_ex(polys, areas_other), // reduce layer areas inward with beams from other mesh + areas_here) // extend layer areas outward with newly added beams + , stInternal); + } + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/Feature/Interlocking/InterlockingGenerator.hpp b/src/libslic3r/Feature/Interlocking/InterlockingGenerator.hpp new file mode 100644 index 0000000000..16e95b9c5f --- /dev/null +++ b/src/libslic3r/Feature/Interlocking/InterlockingGenerator.hpp @@ -0,0 +1,172 @@ +// Copyright (c) 2022 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef INTERLOCKING_GENERATOR_HPP +#define INTERLOCKING_GENERATOR_HPP + +#include "libslic3r/Print.hpp" +#include "VoxelUtils.hpp" + +namespace Slic3r { + +/*! + * Class for generating an interlocking structure between two adjacent models of a different extruder. + * + * The structure consists of horizontal beams of the two materials interlaced. + * In the z direction the direction of these beams is alternated with 90*. + * + * Example with two materials # and O + * Even beams: Odd beams: + * ###### ##OO##OO + * OOOOOO ##OO##OO + * ###### ##OO##OO + * OOOOOO ##OO##OO + * + * One material of a single cell of the structure looks like this: + * .-*-. + * .-* *-. + * |*-. *-. + * | *-. *-. + * .-* *-. *-. *-. + * .-* *-. *-. .-*| + * .-* .-* *-. *-.-* | + * |*-. .-* .-* *-. | .-* + * | *-.-* .-* *-|-* + * *-. | .-* + * *-|-* + * + * We set up a voxel grid of (2*beam_w,2*beam_w,2*beam_h) and mark all the voxels which contain both meshes. + * We then remove all voxels which also contain air, so that the interlocking pattern will not be visible from the outside. + * We then generate and combine the polygons for each voxel and apply those areas to the outlines ofthe meshes. + */ +class InterlockingGenerator +{ +public: + /*! + * Generate an interlocking structure between each two adjacent meshes. + */ + static void generate_interlocking_structure(PrintObject &print_object); + +private: + /*! + * Generate an interlocking structure between two meshes + */ + void generateInterlockingStructure() const; + + /*! + * Private class for storing some variables used in the computation of the interlocking structure between two meshes. + * \param region_a_index The first region + * \param region_b_index The second region + * \param rotation The angle by which to rotate the interlocking pattern + * \param cell_size The size of a voxel cell in (coord_t, coord_t, layer_count) + * \param beam_layer_count The number of layers for the height of the beams + * \param interface_dilation The thicknening kernel for the interface + * \param air_dilation The thickening kernel applied to air so that cells near the outside of the model won't be generated + * \param air_filtering Whether to fully remove all of the interlocking cells which would be visible on the outside (i.e. touching air). + * If no air filtering then those cells will be cut off in the middle of a beam. + */ + InterlockingGenerator(PrintObject& print_object, + const size_t region_a_index, + const size_t region_b_index, + const coord_t beam_width, + const coord_t boundary_avoidance, + const float rotation, + const Vec3crd& cell_size, + const coord_t beam_layer_count, + const DilationKernel& interface_dilation, + const DilationKernel& air_dilation, + const bool air_filtering) + : print_object(print_object) + , region_a_index(region_a_index) + , region_b_index(region_b_index) + , beam_width(beam_width) + , boundary_avoidance(boundary_avoidance) + , vu(cell_size) + , rotation(rotation) + , cell_size(cell_size) + , beam_layer_count(beam_layer_count) + , interface_dilation(interface_dilation) + , air_dilation(air_dilation) + , air_filtering(air_filtering) + {} + + /*! Given two polygons, return the parts that border on air, and grow 'perpendicular' up to 'detect' distance. + * + * \param a The first polygon. + * \param b The second polygon. + * \param detec The expand distance. (Not equal to offset, but a series of small offsets and differences). + * \return A pair of polygons that repressent the 'borders' of a and b, but expanded 'perpendicularly'. + */ + std::pair growBorderAreasPerpendicular(const ExPolygons& a, const ExPolygons& b, const coord_t& detect) const; + + /*! Special handling for thin strips of material. + * + * Expand the meshes into each other where they need it, namely when a thin strip of material needs to be attached. + * \param has_all_meshes Only do this special handling if there's actually microstructure nearby that needs to be adhered to. + */ + void handleThinAreas(const std::unordered_set& has_all_meshes) const; + + /*! + * Compute the voxels overlapping with the shell of both models. + * This includes the walls, but also top/bottom skin. + * + * \param kernel The dilation kernel to give the returned voxel shell more thickness + * \return The shell voxels for mesh a and those for mesh b + */ + std::vector> getShellVoxels(const DilationKernel& kernel) const; + + /*! + * Compute the voxels overlapping with the shell of some layers. + * This includes the walls, but also top/bottom skin. + * + * \param layers The layer outlines for which to compute the shell voxels + * \param kernel The dilation kernel to give the returned voxel shell more thickness + * \param[out] cells The output cells which elong to the shell + */ + void addBoundaryCells(const std::vector& layers, const DilationKernel& kernel, std::unordered_set& cells) const; + + /*! + * Compute the regions occupied by both models. + * + * A morphological close is performed so that we don't register small gaps between the two models as being separate. + * \return layer_regions The computed layer regions + */ + std::vector computeUnionedVolumeRegions() const; + + /*! + * Generate the polygons for the beams of a single cell + * \return cell_area_per_mesh_per_layer The output polygons for each beam + */ + std::vector> generateMicrostructure() const; + + /*! + * Change the outlines of the meshes with the computed interlocking structure. + * + * \param cells The cells where we want to apply the interlocking structure. + * \param layer_regions The total volume of the two meshes combined (and small gaps closed) + */ + void applyMicrostructureToOutlines(const std::unordered_set& cells, const std::vector& layer_regions) const; + + static const coord_t ignored_gap_ = 100u; //!< Distance between models to be considered next to each other so that an interlocking structure will be generated there + + PrintObject& print_object; + const size_t region_a_index; + const size_t region_b_index; + const coord_t beam_width; + const coord_t boundary_avoidance; + + const VoxelUtils vu; + + const float rotation; + const Vec3crd cell_size; + const coord_t beam_layer_count; + const DilationKernel interface_dilation; + const DilationKernel air_dilation; + // Whether to fully remove all of the interlocking cells which would be visible on the outside. If no air filtering then those cells + // will be cut off midway in a beam. + const bool air_filtering; +}; + +} // namespace Slic3r + +#endif // INTERLOCKING_GENERATOR_HPP diff --git a/src/libslic3r/Feature/Interlocking/VoxelUtils.cpp b/src/libslic3r/Feature/Interlocking/VoxelUtils.cpp new file mode 100644 index 0000000000..e0f759f693 --- /dev/null +++ b/src/libslic3r/Feature/Interlocking/VoxelUtils.cpp @@ -0,0 +1,181 @@ +// Copyright (c) 2022 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#include + +#include "VoxelUtils.hpp" +#include "libslic3r/Geometry.hpp" +#include "libslic3r/Fill/FillRectilinear.hpp" +#include "libslic3r/Surface.hpp" + +namespace Slic3r +{ + +DilationKernel::DilationKernel(GridPoint3 kernel_size, DilationKernel::Type type) : kernel_size_(kernel_size) +{ + coord_t mult = kernel_size.x() * kernel_size.y() * kernel_size.z(); // multiplier for division to avoid rounding and to avoid use of floating point numbers + relative_cells_.reserve(mult); + GridPoint3 half_kernel = kernel_size / 2; + + GridPoint3 start = -half_kernel; + GridPoint3 end = kernel_size - half_kernel; + for (coord_t x = start.x(); x < end.x(); x++) + { + for (coord_t y = start.y(); y < end.y(); y++) + { + for (coord_t z = start.z(); z < end.z(); z++) + { + GridPoint3 current(x, y, z); + if (type != Type::CUBE) + { + GridPoint3 limit((x < 0) ? start.x() : end.x() - 1, (y < 0) ? start.y() : end.y() - 1, (z < 0) ? start.z() : end.z() - 1); + if (limit.x() == 0) + limit.x() = 1; + if (limit.y() == 0) + limit.y() = 1; + if (limit.z() == 0) + limit.z() = 1; + const GridPoint3 rel_dists = (mult * current).array() / limit.array(); + if ((type == Type::DIAMOND && rel_dists.x() + rel_dists.y() + rel_dists.z() > mult) || (type == Type::PRISM && rel_dists.x() + rel_dists.y() > mult)) + { + continue; // don't consider this cell + } + } + relative_cells_.emplace_back(x, y, z); + } + } + } +} + +bool VoxelUtils::walkLine(Vec3crd start, Vec3crd end, const std::function& process_cell_func) const +{ + Vec3crd diff = end - start; + + const GridPoint3 start_cell = toGridPoint(start); + const GridPoint3 end_cell = toGridPoint(end); + if (start_cell == end_cell) + { + return process_cell_func(start_cell); + } + + Vec3crd current_cell = start_cell; + while (true) + { + bool continue_ = process_cell_func(current_cell); + + if (! continue_) + { + return false; + } + + int stepping_dim = -1; // dimension in which the line next exits the current cell + double percentage_along_line = std::numeric_limits::max(); + for (int dim = 0; dim < 3; dim++) + { + if (diff[dim] == 0) + { + continue; + } + coord_t crossing_boundary = toLowerCoord(current_cell[dim], dim) + (diff[dim] > 0) * cell_size_[dim]; + double percentage_along_line_here = (crossing_boundary - start[dim]) / static_cast(diff[dim]); + if (percentage_along_line_here < percentage_along_line) + { + percentage_along_line = percentage_along_line_here; + stepping_dim = dim; + } + } + assert(stepping_dim != -1); + if (percentage_along_line > 1.0) + { + // next cell is beyond the end + return true; + } + current_cell[stepping_dim] += (diff[stepping_dim] > 0) ? 1 : -1; + } + return true; +} + + +bool VoxelUtils::walkPolygons(const ExPolygon& polys, coord_t z, const std::function& process_cell_func) const +{ + for (const Polygon& poly : to_polygons(polys)) + { + Point last = poly.back(); + for (Point p : poly) + { + bool continue_ = walkLine(Vec3crd(last.x(), last.y(), z), Vec3crd(p.x(), p.y(), z), process_cell_func); + if (! continue_) + { + return false; + } + last = p; + } + } + return true; +} + +bool VoxelUtils::walkDilatedPolygons(const ExPolygon& polys, coord_t z, const DilationKernel& kernel, const std::function& process_cell_func) const +{ + ExPolygon translated = polys; + GridPoint3 k = kernel.kernel_size_; + k.x() %= 2; + k.y() %= 2; + k.z() %= 2; + const Vec3crd translation = (Vec3crd(1, 1, 1) - k).array() * cell_size_.array() / 2; + if (translation.x() && translation.y()) + { + translated.translate(Point(translation.x(), translation.y())); + } + return walkPolygons(translated, z + translation.z(), dilate(kernel, process_cell_func)); +} + +bool VoxelUtils::_walkAreas(const ExPolygon &ex_polygon, coord_t z, const std::function &process_cell_func) const +{ + Points grid_points; + try { + const BoundingBox ex_polygon_bbox = get_extents(ex_polygon); + grid_points = sample_grid_pattern(ex_polygon, cell_size_.x(), ex_polygon_bbox); + } catch (InfillFailedException &) { + BOOST_LOG_TRIVIAL(warning) << "Sampling ExPolygon failed."; + } + + const Vec3crd grid_point_offset(cell_size_.x() / 2, cell_size_.y() / 2, z); + for (const Point &grid_point : grid_points) { + if (const bool continue_ = process_cell_func(toGridPoint(grid_point, grid_point_offset)); !continue_) { + return false; + } + } + + return true; +} + +bool VoxelUtils::walkDilatedAreas(const ExPolygon& polys, coord_t z, const DilationKernel& kernel, const std::function& process_cell_func) const +{ + ExPolygon translated = polys; + GridPoint3 k = kernel.kernel_size_; + k.x() %= 2; + k.y() %= 2; + k.z() %= 2; + const Vec3crd translation = (Vec3crd(1, 1, 1) - k).array() * cell_size_.array() / 2 // offset half a cell when using an even kernel + - cell_size_.array() / 2; // offset half a cell so that the dots of spreadDotsArea are centered on the middle of the cell isntead of the lower corners. + if (translation.x() && translation.y()) + { + translated.translate(Point(translation.x(), translation.y())); + } + return _walkAreas(translated, z + translation.z(), dilate(kernel, process_cell_func)); +} + +std::function VoxelUtils::dilate(const DilationKernel& kernel, const std::function& process_cell_func) const +{ + return [&process_cell_func, &kernel](GridPoint3 loc) + { + for (const GridPoint3& rel : kernel.relative_cells_) + { + bool continue_ = process_cell_func(loc + rel); + if (! continue_) + return false; + } + return true; + }; +} +} // namespace cura diff --git a/src/libslic3r/Feature/Interlocking/VoxelUtils.hpp b/src/libslic3r/Feature/Interlocking/VoxelUtils.hpp new file mode 100644 index 0000000000..387b2de262 --- /dev/null +++ b/src/libslic3r/Feature/Interlocking/VoxelUtils.hpp @@ -0,0 +1,204 @@ +// Copyright (c) 2022 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef UTILS_VOXEL_UTILS_H +#define UTILS_VOXEL_UTILS_H + +#include + +#include "libslic3r/Polygon.hpp" +#include "libslic3r/ExPolygon.hpp" + +namespace Slic3r +{ + +using GridPoint3 = Vec3crd; + +/*! + * Class for holding the relative positiongs wrt a reference cell on which to perform a dilation. + */ +struct DilationKernel +{ + /*! + * A cubic kernel checks all voxels in a cube around a reference voxel. + * _____ + * |\ ___\ + * | | | + * \|____| + * + * A diamond kernel uses a manhattan distance to create a diamond shape around a reference voxel. + * /|\ + * /_|_\ + * \ | / + * \|/ + * + * A prism kernel is diamond in XY, but extrudes straight in Z around a reference voxel. + * / \ + * / \ + * |\ /| + * | \ / | + * | | | + * \ | / + * \|/ + */ + enum class Type + { + CUBE, + DIAMOND, + PRISM + }; + GridPoint3 kernel_size_; //!< Size of the kernel in number of voxel cells + std::vector relative_cells_; //!< All offset positions relative to some reference cell which is to be dilated + + DilationKernel(GridPoint3 kernel_size, Type type); +}; + +/*! + * Utility class for walking over a 3D voxel grid. + * + * Contains the math for intersecting voxels with lines, polgons, areas, etc. + */ +class VoxelUtils +{ +public: + using grid_coord_t = coord_t; + + Vec3crd cell_size_; + + VoxelUtils(Vec3crd cell_size) + : cell_size_(cell_size) + { + } + + /*! + * Process voxels which a line segment crosses. + * + * \param start Start point of the line + * \param end End point of the line + * \param process_cell_func Function to perform on each cell the line crosses + * \return Whether executing was stopped short as indicated by the \p cell_processing_function + */ + bool walkLine(Vec3crd start, Vec3crd end, const std::function& process_cell_func) const; + + /*! + * Process voxels which the line segments of a polygon crosses. + * + * \warning Voxels may be processed multiple times! + * + * \param polys The polygons to walk + * \param z The height at which the polygons occur + * \param process_cell_func Function to perform on each voxel cell + * \return Whether executing was stopped short as indicated by the \p cell_processing_function + */ + bool walkPolygons(const ExPolygon& polys, coord_t z, const std::function& process_cell_func) const; + + /*! + * Process voxels near the line segments of a polygon. + * For each voxel the polygon crosses we process each of the offset voxels according to the kernel. + * + * \warning Voxels may be processed multiple times! + * + * \param polys The polygons to walk + * \param z The height at which the polygons occur + * \param process_cell_func Function to perform on each voxel cell + * \return Whether executing was stopped short as indicated by the \p cell_processing_function + */ + bool walkDilatedPolygons(const ExPolygon& polys, coord_t z, const DilationKernel& kernel, const std::function& process_cell_func) const; + bool walkDilatedPolygons(const ExPolygons& polys, coord_t z, const DilationKernel& kernel, const std::function& process_cell_func) const + { + for (const auto & poly : polys) { + if (!walkDilatedPolygons(poly, z, kernel, process_cell_func)) { + return false; + } + } + + return true; + } + +private: + /*! + * \warning the \p polys is assumed to be translated by half the cell_size in xy already + */ + bool _walkAreas(const ExPolygon &ex_polygon, coord_t z, const std::function &process_cell_func) const; + +public: + /*! + * Process all voxels inside the area of a polygons object. + * For each voxel inside the polygon we process each of the offset voxels according to the kernel. + * + * \warning The voxels along the area are not processed. Thin areas might not process any voxels at all. + * + * \param polys The area to fill + * \param z The height at which the polygons occur + * \param process_cell_func Function to perform on each voxel cell + * \return Whether executing was stopped short as indicated by the \p cell_processing_function + */ + bool walkDilatedAreas(const ExPolygon& polys, coord_t z, const DilationKernel& kernel, const std::function& process_cell_func) const; + bool walkDilatedAreas(const ExPolygons& polys, coord_t z, const DilationKernel& kernel, const std::function& process_cell_func) const + { + for (const auto & poly : polys) { + if (!walkDilatedAreas(poly, z, kernel, process_cell_func)) { + return false; + } + } + + return true; + } + + /*! + * Dilate with a kernel. + * + * Extends the \p process_cell_func, so that for each cell we process nearby cells as well. + * + * Apply this function to a process_cell_func to create a new process_cell_func which applies the effect to nearby voxels as well. + * + * \param kernel The offset positions relative to the input of \p process_cell_func + * \param process_cell_func Function to perform on each voxel cell + */ + std::function dilate(const DilationKernel& kernel, const std::function& process_cell_func) const; + + GridPoint3 toGridPoint(const Point &point, const Vec3crd &offset) const + { + return toGridPoint(Vec3crd(point.x(), point.y(), 0) + offset); + } + + GridPoint3 toGridPoint(const Vec3crd& point) const + { + return GridPoint3(toGridCoord(point.x(), 0), toGridCoord(point.y(), 1), toGridCoord(point.z(), 2)); + } + + grid_coord_t toGridCoord(const coord_t& coord, const size_t dim) const + { + assert(dim < 3); + return coord / cell_size_[dim] - (coord < 0); + } + + Vec3crd toLowerCorner(const GridPoint3& location) const + { + return Vec3crd(toLowerCoord(location.x(), 0), toLowerCoord(location.y(), 1), toLowerCoord(location.z(), 2)); + } + + coord_t toLowerCoord(const grid_coord_t& grid_coord, const size_t dim) const + { + assert(dim < 3); + return grid_coord * cell_size_[dim]; + } + + /*! + * Returns a rectangular polygon equal to the cross section of a voxel cell at coordinate \p p + */ + Polygon toPolygon(const GridPoint3 p) const + { + Polygon ret; + Vec3crd c = toLowerCorner(p); + ret.append({c.x(), c.y()}); + ret.append({c.x() + cell_size_.x(), c.y()}); + ret.append({c.x() + cell_size_.x(), c.y() + cell_size_.y()}); + ret.append({c.x(), c.y() + cell_size_.y()}); + return ret; + } +}; + +} // namespace Slic3r + +#endif // UTILS_VOXEL_UTILS_H diff --git a/src/libslic3r/Fill/FillRectilinear.cpp b/src/libslic3r/Fill/FillRectilinear.cpp index 23885fbd02..eb06700566 100644 --- a/src/libslic3r/Fill/FillRectilinear.cpp +++ b/src/libslic3r/Fill/FillRectilinear.cpp @@ -41,7 +41,7 @@ #endif #if defined(SLIC3R_DEBUG) || defined(INFILL_DEBUG_OUTPUT) - #include "SVG.hpp" + #include "libslic3r/SVG.hpp" #endif #include diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 42d72a4de3..2de05916bf 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -2077,6 +2077,7 @@ std::vector> segmentation_by_painting(const PrintObject const size_t num_facets_states, const float segmentation_max_width, const float segmentation_interlocking_depth, + const bool segmentation_interlocking_beam, const IncludeTopAndBottomLayers include_top_and_bottom_layers, const std::function &throw_on_cancel_callback) { @@ -2244,7 +2245,7 @@ std::vector> segmentation_by_painting(const PrintObject throw_on_cancel_callback(); } - if (segmentation_max_width > 0.f) { + if ((segmentation_max_width > 0.f || segmentation_interlocking_depth > 0.f) && !segmentation_interlocking_beam) { cut_segmented_layers(input_expolygons, segmented_regions, scaled(segmentation_max_width), scaled(segmentation_interlocking_depth), throw_on_cancel_callback); throw_on_cancel_callback(); } @@ -2266,12 +2267,13 @@ std::vector> multi_material_segmentation_by_painting(con const size_t num_facets_states = print_object.print()->config().nozzle_diameter.size() + 1; const float max_width = float(print_object.config().mmu_segmented_region_max_width.value); const float interlocking_depth = float(print_object.config().mmu_segmented_region_interlocking_depth.value); + const bool interlocking_beam = print_object.config().interlocking_beam.value; const auto extract_facets_info = [](const ModelVolume &mv) -> ModelVolumeFacetsInfo { return {mv.mm_segmentation_facets, mv.is_mm_painted(), false}; }; - return segmentation_by_painting(print_object, extract_facets_info, num_facets_states, max_width, interlocking_depth, IncludeTopAndBottomLayers::Yes, throw_on_cancel_callback); + return segmentation_by_painting(print_object, extract_facets_info, num_facets_states, max_width, interlocking_depth, interlocking_beam, IncludeTopAndBottomLayers::Yes, throw_on_cancel_callback); } // Returns fuzzy skin segmentation based on painting in fuzzy skin segmentation gizmo @@ -2290,7 +2292,7 @@ std::vector> fuzzy_skin_segmentation_by_painting(const P max_external_perimeter_width = std::max(max_external_perimeter_width, region.flow(print_object, frExternalPerimeter, print_object.config().layer_height).width()); } - return segmentation_by_painting(print_object, extract_facets_info, num_facets_states, max_external_perimeter_width, 0.f, IncludeTopAndBottomLayers::No, throw_on_cancel_callback); + return segmentation_by_painting(print_object, extract_facets_info, num_facets_states, max_external_perimeter_width, 0.f, false, IncludeTopAndBottomLayers::No, throw_on_cancel_callback); } diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 8e226e2239..9dc76a45c9 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -511,7 +511,7 @@ static std::vector s_Preset_print_options { "wall_distribution_count", "min_feature_size", "min_bead_width", "top_one_perimeter_type", "only_one_perimeter_first_layer", "automatic_extrusion_widths", "automatic_infill_combination", "automatic_infill_combination_max_layer_height", - "bed_temperature_extruder" + "bed_temperature_extruder", "interlocking_beam", "interlocking_orientation", "interlocking_beam_layer_count", "interlocking_depth", "interlocking_boundary_avoidance", "interlocking_beam_width", }; static std::vector s_Preset_filament_options { diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index c35d6ae652..f14110723e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2000,6 +2000,56 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); + def = this->add("interlocking_beam", coBool); + def->label = L("Use beam interlocking"); + def->tooltip = L("Generate interlocking beam structure at the locations where different filaments touch. This improves the adhesion between filaments, especially models printed in different materials."); + def->category = L("Advanced"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("interlocking_beam_width", coFloat); + def->label = L("Interlocking beam width"); + def->tooltip = L("The width of the interlocking structure beams."); + def->sidetext = L("mm"); + def->min = 0.1f; + def->category = L("Advanced"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.8)); + + def = this->add("interlocking_orientation", coFloat); + def->label = L("Interlocking direction"); + def->tooltip = L("Orientation of interlock beams."); + def->sidetext = L("°"); + def->min = 0; + def->max = 360; + def->category = L("Advanced"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(22.5)); + + def = this->add("interlocking_beam_layer_count", coInt); + def->label = L("Interlocking beam layers"); + def->tooltip = L("The height of the beams of the interlocking structure, measured in number of layers. Less layers is stronger, but more prone to defects."); + def->min = 1; + def->category = L("Advanced"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(2)); + + def = this->add("interlocking_depth", coInt); + def->label = L("Interlocking depth"); + def->tooltip = L("The distance from the boundary between filaments to generate interlocking structure, measured in cells. Too few cells will result in poor adhesion."); + def->min = 1; + def->category = L("Advanced"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(2)); + + def = this->add("interlocking_boundary_avoidance", coInt); + def->label = L("Interlocking boundary avoidance"); + def->tooltip = L("The distance from the outside of a model where interlocking structures will not be generated, measured in cells."); + def->min = 0; + def->category = L("Advanced"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(2)); + def = this->add("ironing_type", coEnum); def->label = L("Ironing Type"); def->category = L("Ironing"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index f860caf4c8..3106819feb 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -681,6 +681,13 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, thick_bridges)) ((ConfigOptionFloat, xy_size_compensation)) ((ConfigOptionBool, wipe_into_objects)) + + ((ConfigOptionBool, interlocking_beam)) + ((ConfigOptionFloat, interlocking_beam_width)) + ((ConfigOptionFloat, interlocking_orientation)) + ((ConfigOptionInt, interlocking_beam_layer_count)) + ((ConfigOptionInt, interlocking_depth)) + ((ConfigOptionInt, interlocking_boundary_avoidance)) ) PRINT_CONFIG_CLASS_DEFINE( diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 0fb31fe22b..dc0b6e454a 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -760,7 +760,13 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "raft_layers" || opt_key == "raft_contact_distance" || opt_key == "slice_closing_radius" - || opt_key == "slicing_mode") { + || opt_key == "slicing_mode" + || opt_key == "interlocking_beam" + || opt_key == "interlocking_orientation" + || opt_key == "interlocking_beam_layer_count" + || opt_key == "interlocking_depth" + || opt_key == "interlocking_boundary_avoidance" + || opt_key == "interlocking_beam_width") { steps.emplace_back(posSlice); } else if ( opt_key == "elefant_foot_compensation" diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 2c043f840e..19fa3d96bd 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -22,6 +22,7 @@ #include "Print.hpp" #include "ShortestPath.hpp" #include "admesh/stl.h" +#include "libslic3r/Feature/Interlocking/InterlockingGenerator.hpp" #include "libslic3r/BoundingBox.hpp" #include "libslic3r/ExPolygon.hpp" #include "libslic3r/Exception.hpp" @@ -923,6 +924,12 @@ void PrintObject::slice_volumes() apply_fuzzy_skin_segmentation(*this, [print]() { print->throw_if_canceled(); }); } + if (m_config.interlocking_beam) { + BOOST_LOG_TRIVIAL(debug) << "Slicing volumes - Applying multi-material interlocking"; + InterlockingGenerator::generate_interlocking_structure(*this); + m_print->throw_if_canceled(); + } + BOOST_LOG_TRIVIAL(debug) << "Slicing volumes - make_slices in parallel - begin"; { // Compensation value, scaled. Only applying the negative scaling here, as the positive scaling has already been applied during slicing. diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 04b9e44ca3..888a9906c7 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -396,9 +396,6 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "wipe_tower_bridging", "wipe_tower_no_sparse_layers", "single_extruder_multi_material_priming" }) toggle_field(el, have_wipe_tower); - bool have_non_zero_mmu_segmented_region_max_width = config->opt_float("mmu_segmented_region_max_width") > 0.; - toggle_field("mmu_segmented_region_interlocking_depth", have_non_zero_mmu_segmented_region_max_width); - toggle_field("avoid_crossing_curled_overhangs", !config->opt_bool("avoid_crossing_perimeters")); toggle_field("avoid_crossing_perimeters", !config->opt_bool("avoid_crossing_curled_overhangs")); @@ -423,6 +420,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) toggle_field("scarf_seam_length", uses_scarf_seam); toggle_field("scarf_seam_max_segment_length", uses_scarf_seam); toggle_field("scarf_seam_on_inner_perimeters", uses_scarf_seam); + + bool use_beam_interlocking = config->opt_bool("interlocking_beam"); + toggle_field("interlocking_beam_width", use_beam_interlocking); + toggle_field("interlocking_orientation", use_beam_interlocking); + toggle_field("interlocking_beam_layer_count", use_beam_interlocking); + toggle_field("interlocking_depth", use_beam_interlocking); + toggle_field("interlocking_boundary_avoidance", use_beam_interlocking); + toggle_field("mmu_segmented_region_max_width", !use_beam_interlocking); + + bool have_non_zero_mmu_segmented_region_max_width = !use_beam_interlocking && config->opt_float("mmu_segmented_region_max_width") > 0.; + toggle_field("mmu_segmented_region_interlocking_depth", have_non_zero_mmu_segmented_region_max_width); } void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config) diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index fe00dd229a..d1012a8313 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -305,9 +305,11 @@ void Field::get_value_by_opt_type(wxString& str, const bool check_value/* = true } } else { - show_error(m_parent, _L("Input value is out of range")); - if (m_opt.min > val) val = m_opt.min; - if (val > m_opt.max) val = m_opt.max; + if (val < (m_opt.min - EPSILON) || val > (m_opt.max + EPSILON)) { + show_error(m_parent, _L("Input value is out of range")); + } + + val = std::clamp(static_cast(val), m_opt.min, m_opt.max); set_value(double_to_string(val), true); } } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index f8f3751759..5a95888607 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1662,6 +1662,13 @@ void TabPrint::build() optgroup->append_single_option_line("mmu_segmented_region_max_width"); optgroup->append_single_option_line("mmu_segmented_region_interlocking_depth"); + optgroup->append_single_option_line("interlocking_beam"); + optgroup->append_single_option_line("interlocking_beam_width"); + optgroup->append_single_option_line("interlocking_orientation"); + optgroup->append_single_option_line("interlocking_beam_layer_count"); + optgroup->append_single_option_line("interlocking_depth"); + optgroup->append_single_option_line("interlocking_boundary_avoidance"); + page = add_options_page(L("Advanced"), "wrench"); optgroup = page->new_optgroup(L("Extrusion width")); optgroup->append_single_option_line("extrusion_width");