diff --git a/CMakeLists.txt b/CMakeLists.txt index 28ed81aa78..317491f60c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,7 +73,7 @@ set(engine_SRCS # Except main.cpp. src/Scene.cpp src/SkeletalTrapezoidation.cpp src/SkeletalTrapezoidationGraph.cpp - src/skin.cpp + src/SkinInfillAreaComputation.cpp src/SkirtBrim.cpp src/SupportInfillPart.cpp src/Slice.cpp diff --git a/include/FffPolygonGenerator.h b/include/FffPolygonGenerator.h index 49342b57fe..6118c7d528 100644 --- a/include/FffPolygonGenerator.h +++ b/include/FffPolygonGenerator.h @@ -148,6 +148,8 @@ class FffPolygonGenerator : public NoCopy */ void processSkinsAndInfill(SliceMeshStorage& mesh, const LayerIndex layer_nr, bool process_infill); + void processSkinsInfillWalls(SliceMeshStorage& mesh, const LayerIndex layer_nr, bool process_infill); + /*! * Generate the polygons where the draft screen should be. * diff --git a/include/skin.h b/include/SkinInfillAreaComputation.h similarity index 92% rename from include/skin.h rename to include/SkinInfillAreaComputation.h index db1140fa09..ddad277ce6 100644 --- a/include/skin.h +++ b/include/SkinInfillAreaComputation.h @@ -1,22 +1,40 @@ -// Copyright (c) 2021 Ultimaker B.V. +// Copyright (c) 2026 Ultimaker B.V. // CuraEngine is released under the terms of the AGPLv3 or higher. -#ifndef SKIN_H -#define SKIN_H +#ifndef SKININFILLAREACOMPUTATION_H +#define SKININFILLAREACOMPUTATION_H #include +#include "geometry/Shape.h" #include "settings/types/LayerIndex.h" #include "utils/Coord_t.h" namespace cura { -class Shape; class SkinPart; class SliceLayerPart; class SliceMeshStorage; +enum class SliceAreaType +{ + Roofing, + Flooring, + Skin, + Infill +}; + +struct SliceArea +{ + using Ptr = std::shared_ptr; + + SliceAreaType type{ SliceAreaType::Infill }; + Shape outline; + size_t walls{ 0 }; + SliceArea::Ptr inner_area; // Inner area that should be also be printed with walls +}; + /*! * Class containing all skin and infill area computation functions */ @@ -40,6 +58,8 @@ class SkinInfillAreaComputation */ void generateSkinsAndInfill(); + SliceArea::Ptr generateRawAreas(SliceLayerPart& part); + /*! * \brief Combines the infill of multiple layers for a specified mesh. * @@ -98,7 +118,7 @@ class SkinInfillAreaComputation * above. The input is the area within the inner walls (or an empty Polygons * object). */ - void calculateTopSkin(const SliceLayerPart& part, Shape& upskin); + Shape calculateTopSkin(const SliceLayerPart& part, Shape& upskin, const size_t layer_count); /*! * \brief Calculate the basic areas which have air below. @@ -107,7 +127,7 @@ class SkinInfillAreaComputation * layers above. The input is the area within the inner walls (or an empty * Polygons object). */ - void calculateBottomSkin(const SliceLayerPart& part, Shape& downskin); + void calculateBottomSkin(const SliceLayerPart& part, Shape& downskin, const size_t layer_count); /*! * Apply skin expansion: @@ -194,4 +214,4 @@ class SkinInfillAreaComputation } // namespace cura -#endif // SKIN_H +#endif 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 3c470ca675..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. diff --git a/src/FffPolygonGenerator.cpp b/src/FffPolygonGenerator.cpp index ad0fb75fed..05b09b9ce5 100644 --- a/src/FffPolygonGenerator.cpp +++ b/src/FffPolygonGenerator.cpp @@ -24,7 +24,7 @@ #include "multiVolumes.h" #include "PrintFeature.h" #include "raft.h" -#include "skin.h" +#include "SkinInfillAreaComputation.h" #include "SkirtBrim.h" #include "Slice.h" #include "TextureDataProvider.h" @@ -452,9 +452,9 @@ void FffPolygonGenerator::processBasicWallsSkinInfill( const std::vector& mesh_order, ProgressStageEstimator& inset_skin_progress_estimate) { - size_t mesh_idx = mesh_order[mesh_order_idx]; + const size_t mesh_idx = mesh_order[mesh_order_idx]; SliceMeshStorage& mesh = *storage.meshes[mesh_idx]; - size_t mesh_layer_count = mesh.layers.size(); + const size_t mesh_layer_count = mesh.layers.size(); if (mesh.settings.get("infill_mesh")) { processInfillMesh(storage, mesh_order_idx, mesh_order); @@ -496,20 +496,6 @@ void FffPolygonGenerator::processBasicWallsSkinInfill( } } guarded_progress = { inset_skin_progress_estimate }; - // walls - cura::parallel_for( - 0, - mesh_layer_count, - [&](size_t layer_number) - { - spdlog::debug("Processing insets for layer {} of {}", layer_number, mesh.layers.size()); - processWalls(mesh, layer_number); - guarded_progress++; - }); - - ProgressEstimatorLinear* skin_estimator = new ProgressEstimatorLinear(mesh_layer_count); - mesh_inset_skin_progress_estimator->nextStage(skin_estimator); - bool process_infill = mesh.settings.get("infill_line_distance") > 0; if (! process_infill) { // do process infill anyway if it's modified by modifier meshes @@ -530,6 +516,7 @@ void FffPolygonGenerator::processBasicWallsSkinInfill( } } + // skin & infill const Settings& mesh_group_settings = Application::getInstance().current_slice_->scene.current_mesh_group->settings; bool magic_spiralize = mesh_group_settings.get("magic_spiralize"); @@ -539,6 +526,19 @@ void FffPolygonGenerator::processBasicWallsSkinInfill( mesh_max_initial_bottom_layer_count = std::max(mesh_max_initial_bottom_layer_count, mesh.settings.get("initial_bottom_layers")); } + cura::parallel_for( + 0, + mesh_layer_count, + [&](size_t layer_number) + { + spdlog::debug("Processing skins and infill layer {} of {}", layer_number, mesh.layers.size()); + if (! magic_spiralize || layer_number < mesh_max_initial_bottom_layer_count) // Only generate up/down skin and infill for the first X layers when spiralize is chosen. + { + processSkinsInfillWalls(mesh, layer_number, process_infill); + } + guarded_progress++; + }); + guarded_progress.reset(); cura::parallel_for( 0, @@ -546,12 +546,27 @@ void FffPolygonGenerator::processBasicWallsSkinInfill( [&](size_t layer_number) { spdlog::debug("Processing skins and infill layer {} of {}", layer_number, mesh.layers.size()); - if (! magic_spiralize || layer_number < mesh_max_initial_bottom_layer_count) // Only generate up/downskin and infill for the first X layers when spiralize is choosen. + if (! magic_spiralize || layer_number < mesh_max_initial_bottom_layer_count) // Only generate up/down skin and infill for the first X layers when spiralize is chosen. { processSkinsAndInfill(mesh, layer_number, process_infill); } guarded_progress++; }); + + // walls + guarded_progress.reset(); + cura::parallel_for( + 0, + mesh_layer_count, + [&](size_t layer_number) + { + spdlog::debug("Processing insets for layer {} of {}", layer_number, mesh.layers.size()); + processWalls(mesh, layer_number); + guarded_progress++; + }); + + ProgressEstimatorLinear* skin_estimator = new ProgressEstimatorLinear(mesh_layer_count); + mesh_inset_skin_progress_estimator->nextStage(skin_estimator); } void FffPolygonGenerator::processInfillMesh(SliceDataStorage& storage, const size_t mesh_order_idx, const std::vector& mesh_order) @@ -822,6 +837,16 @@ void FffPolygonGenerator::removeEmptyFirstLayers(SliceDataStorage& storage, size } } +void FffPolygonGenerator::processSkinsInfillWalls(SliceMeshStorage& mesh, const LayerIndex layer_nr, bool process_infill) +{ + SkinInfillAreaComputation skin_infill_area_computation(layer_nr, mesh, process_infill); + + for (SliceLayerPart& part : mesh.layers[layer_nr].parts) + { + skin_infill_area_computation.generateRawAreas(part); + } +} + /* * This function is executed in a parallel region based on layer_nr. * When modifying make sure any changes does not introduce data races. @@ -1200,5 +1225,4 @@ void FffPolygonGenerator::processFuzzyWalls(SliceMeshStorage& mesh) } } - } // namespace cura diff --git a/src/skin.cpp b/src/SkinInfillAreaComputation.cpp similarity index 83% rename from src/skin.cpp rename to src/SkinInfillAreaComputation.cpp index 4daa5421e5..be91a6dd96 100644 --- a/src/skin.cpp +++ b/src/SkinInfillAreaComputation.cpp @@ -1,7 +1,7 @@ // Copyright (c) 2023 UltiMaker // CuraEngine is released under the terms of the AGPLv3 or higher -#include "skin.h" +#include "SkinInfillAreaComputation.h" #include // std::ceil @@ -91,12 +91,113 @@ void SkinInfillAreaComputation::generateSkinsAndInfill() SliceLayer* layer = &mesh_.layers[layer_nr_]; + const Shape outline = layer->getOutlines(); + // SVG svg(fmt::format("/tmp/areas_{}.svg", layer_nr_), AABB(outline), 0.001); + for (SliceLayerPart& part : layer->parts) { + // svg.write(part.outline, { .line = { 0.1 } }); + generateSkinRoofingFlooringFill(part); generateTopAndBottomMostSurfaces(part); + + for (const SkinPart& skin_part : part.skin_parts) + { + // svg.write(skin_part.outline, { .line = { 0.05 } }); + // svg.write(skin_part.roofing_fill, { .surface = { SVG::Color::GREEN, 0.5 } }); + // svg.write(skin_part.flooring_fill, { .surface = { SVG::Color::RED, 0.5 } }); + // svg.write(skin_part.skin_fill, { .surface = { SVG::Color::YELLOW, 0.5 } }); + } + } +} + +SliceArea::Ptr SkinInfillAreaComputation::generateRawAreas(SliceLayerPart& part) +{ + // Make a copy of the outline which we later intersect and union with the resized skins to ensure the resized skin isn't too large or removed completely. + Shape top_skin; + if (top_layer_count_ > 0) + { + top_skin = Shape(part.outline); + } + calculateTopSkin(part, top_skin); + + Shape bottom_skin; + if (bottom_layer_count_ > 0 || layer_nr_ < LayerIndex(initial_bottom_layer_count_)) + { + bottom_skin = Shape(part.outline); + } + calculateBottomSkin(part, bottom_skin); + + applySkinExpansion(part.outline, top_skin, bottom_skin); + + // Now combine the resized top skin and bottom skin. + // Shape skin = top_skin.unionPolygons(bottom_skin); + + skin.removeSmallAreas(MIN_AREA_SIZE); + + 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_expansion = mesh_.settings.get("roofing_expansion"); + + const SliceDataStorage slice_data; + const Shape build_plate = slice_data.getRawMachineBorder(); + + const Shape filled_area_just_above = generateFilledAreaAbove(part, roofing_layer_count); + const Shape filled_area_just_below = generateFilledAreaBelow(part, flooring_layer_count).value_or(build_plate.offset(EPSILON)); + const Shape filled_area_above = generateFilledAreaAbove(part, top_layer_count_); + const Shape filled_area_below = generateFilledAreaBelow(part, bottom_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_expansion * 2).difference(part.outline).unionPolygons(filled_area_just_above.offset(EPSILON)).offset(-roofing_expansion - 2 * EPSILON); + + Shape roofing_area = part.outline.difference(reduced_area_above); + Shape flooring_area = part.outline.intersection(filled_area_just_above).difference(filled_area_just_below); + // Shape skin_area = part.outline.difference(roofing_area).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. + skin = skin.offset(skin_overlap).difference(roofing_area).difference(flooring_area); + roofing_area = roofing_area.offset(skin_overlap); + flooring_area = flooring_area.offset(skin_overlap).difference(roofing_area); + + // Create infill area irrespective if the infill is to be generated or not(would be used for bridging). + const Shape infill = part.outline.difference(skin.offset(EPSILON)).difference(roofing_area.offset(EPSILON)).difference(flooring_area.offset(EPSILON)).offset(-EPSILON); + // part.infill_area = part.inner_area.difference(skin); + // if (process_infill_) + { // process infill when infill density > 0 + // or when other infill meshes want to modify this infill + // generateInfill(part); } + // for (const SingleShape& skin_area_part : skin.splitIntoParts()) + // { + // if (skin_area_part.empty()) + // { + // continue; + // } + // part.skin_parts.emplace_back(); + // part.skin_parts.back().outline = skin_area_part; + // } + + SVG svg(fmt::format("/tmp/areas_{}.svg", layer_nr_), AABB(part.outline), 0.001); + + svg.write(part.outline, { .line = { 0.1 } }); + + // generateSkinRoofingFlooringFill(part); + + // generateTopAndBottomMostSurfaces(part); + + svg.write(skin, { .surface = { SVG::Color::YELLOW, 0.5 } }); + svg.write(top_skin, { .surface = { SVG::Color::GREEN, 0.5 } }); + svg.write(bottom_skin, { .surface = { SVG::Color::RED, 0.5 } }); + svg.write(roofing_area, { .surface = { SVG::Color::MAGENTA, 0.5 } }); + svg.write(flooring_area, { .surface = { SVG::Color::ORANGE, 0.5 } }); + svg.write(infill, { .surface = { SVG::Color::YELLOW, 0.5 } }); + + return {}; } /* @@ -174,17 +275,17 @@ void SkinInfillAreaComputation::generateSkinAndInfillAreas(SliceLayerPart& part) * * this function may only read/write the skin and infill from the *current* layer. */ -void SkinInfillAreaComputation::calculateBottomSkin(const SliceLayerPart& part, Shape& downskin) +Shape SkinInfillAreaComputation::calculateBottomSkin(const SliceLayerPart& part, const size_t layer_count) { - if (bottom_layer_count_ == 0 && initial_bottom_layer_count_ == 0) + if (layer_count == 0) { return; // downskin remains empty } - if (layer_nr_ < LayerIndex(initial_bottom_layer_count_)) + if (layer_nr_ < LayerIndex(layer_count)) { return; // don't subtract anything form the downskin } - LayerIndex bottom_check_start_layer_idx{ std::max(LayerIndex{ 0 }, LayerIndex{ layer_nr_ - bottom_layer_count_ }) }; + LayerIndex bottom_check_start_layer_idx{ std::max(LayerIndex{ 0 }, LayerIndex{ layer_nr_ - layer_count }) }; Shape not_air = getOutlineOnLayer(part, bottom_check_start_layer_idx); if (! no_small_gaps_heuristic_) { @@ -198,22 +299,22 @@ void SkinInfillAreaComputation::calculateBottomSkin(const SliceLayerPart& part, { not_air.removeSmallAreas(min_infill_area); } - downskin = downskin.difference(not_air); // skin overlaps with the walls + return Shape(part.outline).difference(not_air); // skin overlaps with the walls } -void SkinInfillAreaComputation::calculateTopSkin(const SliceLayerPart& part, Shape& upskin) +Shape SkinInfillAreaComputation::calculateTopSkin(const SliceLayerPart& part, const size_t layer_count) { - if (layer_nr_ > LayerIndex(mesh_.layers.size()) - top_layer_count_ || top_layer_count_ <= 0) + if (layer_nr_ > LayerIndex(mesh_.layers.size()) - layer_count || layer_count == 0) { - // If we're in the very top layers (less than top_layer_count from the top of the mesh) everything will be top skin anyway, so no need to generate infill. Just take the - // original inner contour. If top_layer_count is 0, no need to calculate anything either. + // If we're in the very top layers (less than layer_count from the top of the mesh) everything will be top skin anyway, so no need to generate infill. Just take the + // original inner contour. If layer_count is 0, no need to calculate anything either. return; } - Shape not_air = getOutlineOnLayer(part, layer_nr_ + top_layer_count_); + Shape not_air = getOutlineOnLayer(part, layer_nr_ + layer_count); if (! no_small_gaps_heuristic_) { - for (int upskin_layer_nr = layer_nr_ + 1; upskin_layer_nr < layer_nr_ + top_layer_count_; upskin_layer_nr++) + for (int upskin_layer_nr = layer_nr_ + 1; upskin_layer_nr < layer_nr_ + layer_count; upskin_layer_nr++) { not_air = not_air.intersection(getOutlineOnLayer(part, upskin_layer_nr)); } @@ -225,7 +326,7 @@ void SkinInfillAreaComputation::calculateTopSkin(const SliceLayerPart& part, Sha not_air.removeSmallAreas(min_infill_area); } - upskin = upskin.difference(not_air); // skin overlaps with the walls + return Shape(part.outline).difference(not_air); // skin overlaps with the walls } /* 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--)