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 @@
+
+
+
+
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);