diff --git a/doc/skin_parts.svg b/doc/skin_parts.svg new file mode 100644 index 0000000000..5def341e88 --- /dev/null +++ b/doc/skin_parts.svg @@ -0,0 +1,211 @@ + + + + + + + + + outline + + below + + above + + + Roof (by default) + Roof + Floor + Skin + + below_inside + + above_inside + + Roofingextensionarea + + + diff --git a/include/FffGcodeWriter.h b/include/FffGcodeWriter.h index 50483a58ba..3d1b9af378 100644 --- a/include/FffGcodeWriter.h +++ b/include/FffGcodeWriter.h @@ -575,6 +575,7 @@ class FffGcodeWriter : public NoCopy * \param skin_overlap The amount by which to expand the \p area * \param skin density Sets the density of the the skin lines by adjusting the distance between them (normal skin is 1.0) * \param monotonic Whether to order lines monotonically (``true``) or to + * \param is_roofing_flooring Indicates whether we are currently processing a top/bottom layer, or a skin layer * minimise travel moves (``false``). * \param[out] added_something Whether this function added anything to the layer plan * \param fan_speed fan speed override for this skin area @@ -591,6 +592,7 @@ class FffGcodeWriter : public NoCopy const coord_t skin_overlap, const Ratio skin_density, const bool monotonic, + const bool is_roofing_flooring, bool& added_something, double fan_speed = GCodePathConfig::FAN_SPEED_DEFAULT) const; diff --git a/include/layerPart.h b/include/layerPart.h index 3eed7e7b8d..2234f9fa18 100644 --- a/include/layerPart.h +++ b/include/layerPart.h @@ -1,5 +1,5 @@ -//Copyright (c) 2018 Ultimaker B.V. -//CuraEngine is released under the terms of the AGPLv3 or higher. +// Copyright (c) 2025 UltiMaker +// CuraEngine is released under the terms of the AGPLv3 or higher. #ifndef LAYERPART_H #define LAYERPART_H @@ -19,21 +19,9 @@ It's also the first step that stores the result in the "data storage" so all oth namespace cura { -class Settings; -class SliceLayer; class Slicer; -class SlicerLayer; class SliceMeshStorage; -/*! - * \brief Split a layer into parts. - * \param settings The settings to get the settings from (whether to union or - * not). - * \param storageLayer Where to store the parts. - * \param layer The layer to split. - */ -void createLayerWithParts(const Settings& settings, SliceLayer& storageLayer, SlicerLayer* layer); - /*! * \brief Split all layers into parts. * \param mesh The mesh of which to split the layers into parts. @@ -41,6 +29,6 @@ void createLayerWithParts(const Settings& settings, SliceLayer& storageLayer, Sl */ void createLayerParts(SliceMeshStorage& mesh, Slicer* slicer); -}//namespace cura +} // namespace cura -#endif//LAYERPART_H +#endif // LAYERPART_H diff --git a/include/sliceDataStorage.h b/include/sliceDataStorage.h index 75c977a598..21eff6566e 100644 --- a/include/sliceDataStorage.h +++ b/include/sliceDataStorage.h @@ -57,6 +57,14 @@ class SkinPart class SliceLayerPart { public: + enum class WallExposedType + { + LAYER_0, + ROOFING, + SIDE_ONLY, + }; + WallExposedType wall_exposed = WallExposedType::SIDE_ONLY; + AABB boundaryBox; //!< The boundaryBox is an axis-aligned boundary box which is used to quickly check for possible //!< collision between different parts on different layers. It's an optimization used during //!< skin calculations. @@ -461,6 +469,11 @@ class SliceDataStorage : public NoCopy */ Shape getMachineBorder(int extruder_nr = -1) const; + /*! + * @return The raw outer build plate shape without any disallowed area + */ + Shape getRawMachineBorder() const; + void initializePrimeTower(); private: diff --git a/src/FffGcodeWriter.cpp b/src/FffGcodeWriter.cpp index f24d916a21..d631355119 100644 --- a/src/FffGcodeWriter.cpp +++ b/src/FffGcodeWriter.cpp @@ -3473,7 +3473,21 @@ void FffGcodeWriter::processRoofingFlooring( const Ratio skin_density = 1.0; const coord_t skin_overlap = 0; // skinfill already expanded over the roofing areas; don't overlap with perimeters const bool monotonic = mesh.settings.get(settings_names.monotonic); - processSkinPrintFeature(storage, gcode_layer, mesh, extruder_nr, fill, config, pattern, roofing_angle, skin_overlap, skin_density, monotonic, added_something); + constexpr bool is_roofing_flooring = true; + processSkinPrintFeature( + storage, + gcode_layer, + mesh, + extruder_nr, + fill, + config, + pattern, + roofing_angle, + skin_overlap, + skin_density, + monotonic, + is_roofing_flooring, + added_something); } void FffGcodeWriter::processTopBottom( @@ -3641,6 +3655,7 @@ void FffGcodeWriter::processTopBottom( } } const bool monotonic = mesh.settings.get("skin_monotonic"); + constexpr bool is_roofing_flooring = false; processSkinPrintFeature( storage, gcode_layer, @@ -3653,6 +3668,7 @@ void FffGcodeWriter::processTopBottom( skin_overlap, skin_density, monotonic, + is_roofing_flooring, added_something, fan_speed); } @@ -3669,6 +3685,7 @@ void FffGcodeWriter::processSkinPrintFeature( const coord_t skin_overlap, const Ratio skin_density, const bool monotonic, + const bool is_roofing_flooring, bool& added_something, double fan_speed) const { @@ -3679,7 +3696,6 @@ void FffGcodeWriter::processSkinPrintFeature( constexpr int infill_multiplier = 1; constexpr int extra_infill_shift = 0; const size_t wall_line_count = mesh.settings.get("skin_outline_count"); - const coord_t small_area_width = mesh.settings.get("small_skin_width"); const bool zig_zaggify_infill = pattern == EFillMethod::ZIG_ZAG; const bool connect_polygons = mesh.settings.get("connect_skin_polygons"); coord_t max_resolution = mesh.settings.get("meshfix_maximum_resolution"); @@ -3693,6 +3709,7 @@ void FffGcodeWriter::processSkinPrintFeature( constexpr int zag_skip_count = 0; constexpr coord_t pocket_size = 0; const bool small_areas_on_surface = mesh.settings.get("small_skin_on_surface"); + const coord_t small_area_width = (small_areas_on_surface || ! is_roofing_flooring) ? mesh.settings.get("small_skin_width") : 0; const auto& current_layer = mesh.layers[gcode_layer.getLayerNr()]; const auto& exposed_to_air = current_layer.top_surface.areas.unionPolygons(current_layer.bottom_surface); diff --git a/src/WallsComputation.cpp b/src/WallsComputation.cpp index f75c89defa..81dc5aa918 100644 --- a/src/WallsComputation.cpp +++ b/src/WallsComputation.cpp @@ -37,7 +37,10 @@ WallsComputation::WallsComputation(const Settings& settings, const LayerIndex la */ void WallsComputation::generateWalls(SliceLayerPart* part, SectionType section_type) { - size_t wall_count = settings_.get("wall_line_count"); + const std::map wall_count_setting_names({ { SliceLayerPart::WallExposedType::LAYER_0, "wall_line_count_layer_0" }, + { SliceLayerPart::WallExposedType::ROOFING, "wall_line_count_roofing" }, + { SliceLayerPart::WallExposedType::SIDE_ONLY, "wall_line_count" } }); + size_t wall_count = settings_.get(wall_count_setting_names.at(part->wall_exposed)); if (wall_count == 0) // Early out if no walls are to be generated { part->print_outline = part->outline; diff --git a/src/layerPart.cpp b/src/layerPart.cpp index e1754b73fe..bc1448775c 100644 --- a/src/layerPart.cpp +++ b/src/layerPart.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2023 UltiMaker +// Copyright (c) 2025 UltiMaker // CuraEngine is released under the terms of the AGPLv3 or higher. #include "layerPart.h" @@ -28,7 +28,16 @@ It's also the first step that stores the result in the "data storage" so all oth namespace cura { -void createLayerWithParts(const Settings& settings, SliceLayer& storageLayer, SlicerLayer* layer) +/*! + * \brief Split a layer into parts. + * \param settings The settings to get the settings from (whether to union or + * not). + * \param storageLayer Where to store the parts. + * \param layer The layer to split. + * \param bottom_parts The bottom parts of the layer. + * \param top_parts The top parts of the layer. + */ +void createLayerWithParts(const Settings& settings, SliceLayer& storageLayer, SlicerLayer* layer, const Shape& bottom_parts, const Shape& top_parts) { OpenPolylineStitcher::stitch(layer->open_polylines_, storageLayer.open_polylines, layer->polygons_, settings.get("wall_line_width_0")); @@ -65,20 +74,51 @@ void createLayerWithParts(const Settings& settings, SliceLayer& storageLayer, Sl result = layer->polygons_.splitIntoParts(union_layers || union_all_remove_holes); } - for (auto& part : result) + for (auto& main_part : result) { - storageLayer.parts.emplace_back(); - if (part.empty()) + std::map> parts_by_type = { + { SliceLayerPart::WallExposedType::LAYER_0, bottom_parts.splitIntoParts() }, + { SliceLayerPart::WallExposedType::ROOFING, top_parts.difference(bottom_parts).splitIntoParts() }, + { SliceLayerPart::WallExposedType::SIDE_ONLY, main_part.difference(bottom_parts).difference(top_parts).splitIntoParts() }, + }; + + for (auto& [wall_exposed, parts] : parts_by_type) { - continue; + for (auto& part : parts) + { + storageLayer.parts.emplace_back(); + if (part.empty()) + { + continue; + } + auto& back_part = storageLayer.parts.back(); + back_part.wall_exposed = wall_exposed; + back_part.outline = part; + back_part.boundaryBox.calculate(back_part.outline); + if (back_part.outline.empty()) + { + storageLayer.parts.pop_back(); + } + } } - storageLayer.parts.back().outline = part; - storageLayer.parts.back().boundaryBox.calculate(storageLayer.parts.back().outline); - if (storageLayer.parts.back().outline.empty()) + } +} + +Shape getTopOrBottom(int direction, const std::string& setting_name, size_t layer_nr, const std::vector& slayers, const Settings& settings) +{ + Shape result; + if (settings.get(setting_name) != settings.get("wall_line_count") && ! settings.get("magic_spiralize")) + { + result = slayers[layer_nr].polygons_; + const auto next_layer = layer_nr + direction; + if (next_layer >= 0 && next_layer < slayers.size()) { - storageLayer.parts.pop_back(); + constexpr coord_t EPSILON = 5; + const auto wall_line_width = settings.get(layer_nr == 0 ? "wall_line_width_0" : "wall_line_width") - EPSILON; + result = result.offset(-wall_line_width).difference(slayers[next_layer].polygons_).offset(wall_line_width); } } + return result; } void createLayerParts(SliceMeshStorage& mesh, Slicer* slicer) @@ -93,7 +133,12 @@ void createLayerParts(SliceMeshStorage& mesh, Slicer* slicer) { SliceLayer& layer_storage = mesh.layers[layer_nr]; SlicerLayer& slice_layer = slicer->layers[layer_nr]; - createLayerWithParts(mesh.settings, layer_storage, &slice_layer); + createLayerWithParts( + mesh.settings, + layer_storage, + &slice_layer, + layer_nr == 0 ? getTopOrBottom(-1, "wall_line_count_layer_0", layer_nr, slicer->layers, mesh.settings) : Shape(), + getTopOrBottom(+1, "wall_line_count_roofing", layer_nr, slicer->layers, mesh.settings)); }); for (LayerIndex layer_nr = total_layers - 1; layer_nr >= 0; layer_nr--) diff --git a/src/skin.cpp b/src/skin.cpp index def6af7a28..83bc54b090 100644 --- a/src/skin.cpp +++ b/src/skin.cpp @@ -328,28 +328,27 @@ void SkinInfillAreaComputation::generateInfill(SliceLayerPart& part) */ void SkinInfillAreaComputation::generateSkinRoofingFlooringFill(SliceLayerPart& part) { - for (SkinPart& skin_part : part.skin_parts) - { - const size_t roofing_layer_count = std::min(mesh_.settings.get("roofing_layer_count"), mesh_.settings.get("top_layers")); - const size_t flooring_layer_count = std::min(mesh_.settings.get("flooring_layer_count"), mesh_.settings.get("bottom_layers")); - const coord_t skin_overlap = mesh_.settings.get("skin_overlap_mm"); + const size_t roofing_layer_count = std::min(mesh_.settings.get("roofing_layer_count"), mesh_.settings.get("top_layers")); + const size_t flooring_layer_count = std::min(mesh_.settings.get("flooring_layer_count"), mesh_.settings.get("bottom_layers")); + const coord_t skin_overlap = mesh_.settings.get("skin_overlap_mm"); + const coord_t roofing_extension = mesh_.settings.get("roofing_extension"); - const Shape filled_area_above = generateFilledAreaAbove(part, roofing_layer_count); - const std::optional filled_area_below = generateFilledAreaBelow(part, flooring_layer_count); + constexpr coord_t epsilon = 5; + const SliceDataStorage slice_data; + const Shape build_plate = slice_data.getRawMachineBorder(); - // An area that would have nothing below nor above is considered a roof - skin_part.roofing_fill = skin_part.outline.difference(filled_area_above); - if (filled_area_below.has_value()) - { - skin_part.flooring_fill = skin_part.outline.intersection(filled_area_above).difference(*filled_area_below); - skin_part.skin_fill = skin_part.outline.intersection(filled_area_above).intersection(*filled_area_below); - } - else - { - // Mesh part is just above build plate, so it is completely supported - // skin_part.flooring_fill = Shape(); - skin_part.skin_fill = skin_part.outline.intersection(filled_area_above); - } + const Shape filled_area_above = generateFilledAreaAbove(part, roofing_layer_count); + const Shape filled_area_below = generateFilledAreaBelow(part, flooring_layer_count).value_or(build_plate.offset(epsilon)); + + // In order to avoid edge cases, it is safer to create the extended roofing area by reducing the area above. However, we want to avoid reducing the borders, so at this + // point we extend the area above with the build plate area, so that when reducing, the border will still be far away. + const Shape reduced_area_above = build_plate.offset(roofing_extension * 2).difference(part.outline).unionPolygons(filled_area_above.offset(epsilon)).offset(-roofing_extension); + + for (SkinPart& skin_part : part.skin_parts) + { + skin_part.roofing_fill = skin_part.outline.difference(reduced_area_above); + skin_part.flooring_fill = skin_part.outline.intersection(filled_area_above).difference(filled_area_below); + skin_part.skin_fill = skin_part.outline.difference(skin_part.roofing_fill).intersection(filled_area_below); // We remove offsets areas from roofing and flooring anywhere they overlap with skin_fill. // Otherwise, adjacent skin_fill and roofing/flooring would have doubled offset areas. Since they both offset into each other. diff --git a/src/sliceDataStorage.cpp b/src/sliceDataStorage.cpp index d26e2aaa8a..6f2e0981ee 100644 --- a/src/sliceDataStorage.cpp +++ b/src/sliceDataStorage.cpp @@ -611,29 +611,7 @@ Shape SliceDataStorage::getMachineBorder(int checking_extruder_nr) const { const Settings& mesh_group_settings = Application::getInstance().current_slice_->scene.current_mesh_group->settings; - Shape border; - border.emplace_back(); - Polygon& outline = border.back(); - switch (mesh_group_settings.get("machine_shape")) - { - case BuildPlateShape::ELLIPTIC: - { - // Construct an ellipse to approximate the build volume. - const coord_t width = machine_size.max_.x_ - machine_size.min_.x_; - const coord_t depth = machine_size.max_.y_ - machine_size.min_.y_; - constexpr unsigned int circle_resolution = 50; - for (unsigned int i = 0; i < circle_resolution; i++) - { - const double angle = std::numbers::pi * 2 * i / circle_resolution; - outline.emplace_back(machine_size.getMiddle().x_ + std::cos(angle) * width / 2, machine_size.getMiddle().y_ + std::sin(angle) * depth / 2); - } - break; - } - case BuildPlateShape::RECTANGULAR: - default: - outline = machine_size.flatten().toPolygon(); - break; - } + Shape border = getRawMachineBorder(); Shape disallowed_areas = mesh_group_settings.get("machine_disallowed_areas"); disallowed_areas = disallowed_areas.unionPolygons(); // union overlapping disallowed areas @@ -731,6 +709,37 @@ Shape SliceDataStorage::getMachineBorder(int checking_extruder_nr) const return border; } +Shape SliceDataStorage::getRawMachineBorder() const +{ + const Settings& mesh_group_settings = Application::getInstance().current_slice_->scene.current_mesh_group->settings; + + Shape border; + border.emplace_back(); + Polygon& outline = border.back(); + switch (mesh_group_settings.get("machine_shape")) + { + case BuildPlateShape::ELLIPTIC: + { + // Construct an ellipse to approximate the build volume. + const coord_t width = machine_size.max_.x_ - machine_size.min_.x_; + const coord_t depth = machine_size.max_.y_ - machine_size.min_.y_; + constexpr unsigned int circle_resolution = 50; + for (unsigned int i = 0; i < circle_resolution; i++) + { + const double angle = std::numbers::pi * 2 * i / circle_resolution; + outline.emplace_back(machine_size.getMiddle().x_ + std::cos(angle) * width / 2, machine_size.getMiddle().y_ + std::sin(angle) * depth / 2); + } + break; + } + case BuildPlateShape::RECTANGULAR: + default: + outline = machine_size.flatten().toPolygon(); + break; + } + + return border; +} + void SliceDataStorage::initializePrimeTower() { prime_tower_ = PrimeTower::createPrimeTower(*this);