diff --git a/docs/HLSD/prime-tower-sparse-layers.md b/docs/HLSD/prime-tower-sparse-layers.md new file mode 100644 index 0000000000..19802c9169 --- /dev/null +++ b/docs/HLSD/prime-tower-sparse-layers.md @@ -0,0 +1,197 @@ +# Prime tower sparse layers — High Level Design + +## Purpose and scope + +A prime tower exists to absorb filament changes, but it is planned on every +object layer below the topmost change, not only on the layers that purge. The +layers in between carry no filament change and print nothing but a block of the +tower's own footprint to keep its top level. They are called sparse layers, and +on a print with few changes they are most of the tower: they cost time, filament +and a travel to the tower on every layer. + +Two settings trade that cost against something else. `wipe_tower_no_sparse_layers` +drops them, which sinks the tower below the model. `wipe_tower_sparse_layers_combination` +merges runs of them into fewer, thicker layers, which keeps the tower level with +the model. Both are off by default, and with both off the tower prints one layer +per object layer as it always has. + +The decisions belong to tower planning and G-code emission. They do not change +sliced object geometry, but they do change the emitted G-code, the filament and +time estimates, and — for the compacted case — whether a plate is printable at +all. Changing either setting invalidates the tower step. + +## What a sparse layer is + +`ToolOrdering::fill_wipe_tower_partitions` counts the filament changes per layer +and propagates that count downwards, so every layer below the topmost change is +marked as carrying a tower. It then fills any gap between two tower layers, so +the tower is continuous from the bed to its last purge. `wipe_tower_layer_height` +is the distance from the previous tower layer, which is the object's layer height +whenever the tower prints on every layer. + +`Print::_make_wipe_tower` plans one tower layer per such object layer. A layer +whose only call keeps the current filament leaves no toolchange in the plan, and +the layer it generates is a single result whose initial and new tool are equal. +That is what `wipe_tower_layer_is_sparse` recognises, and it is the unit both +settings work on. + +The plan stays one entry per tower layer in every case. The G-code emitter walks +`WipeTowerData::tool_changes` by layer index, advancing once per object layer +that carries a tower, so a planner that removed entries would silently shift +every later layer onto the wrong tower geometry. Layers that print nothing are +therefore still planned and still generated; they are marked, and the emitter +drops them. + +## Shared rules + +Tower planning, G-code emission and the plate validation all have to agree about +which layers print and where. They ask one set of free functions, declared beside +the tower classes, rather than each re-deriving the answer from the raw options: + +- `wipe_tower_sparse_layers_skipped` — whether sparse layers are really dropped. + Smooth timelapse and clumping detection park the nozzle on the tower every + layer, so with either of them on no layer is ever dropped and the option reads + as off everywhere. +- `wipe_tower_sparse_layers_combined` — whether runs are really merged. The same + two rule it out, and so does `wipe_tower_no_sparse_layers`: dropping the layers + outright is the stronger answer to the same problem, so the two settings are + exclusive and the GUI greys out the second while the first is on. +- `wipe_tower_layer_is_sparse`, `wipe_tower_layer_is_combined_away` — per-layer + questions the emitter asks about generated results. +- `compute_compacted_wipe_tower_z` — the tower's print z per planned layer when + it is compacted. +- `combine_sparse_wipe_tower_layers` and its `combine_sparse_wipe_tower_plan` + wrapper — the merge rule, applied to either generator's plan. + +Both tower generators are driven through these. `WipeTower` (Type 1, the block +tower) and `WipeTower2` (Type 2, the default) keep separate plans with the same +per-layer shape — print z, layer height, toolchanges, and a `combined_away` flag +— so one template covers both. + +## Dropping sparse layers + +With `wipe_tower_no_sparse_layers`, the tower only grows on layers that carry a +real change. It therefore falls one layer height behind the object for every +sparse layer, and by the top of a tall print it can sit far below the model. The +nozzle has to reach down to it at each purge. + +`compute_compacted_wipe_tower_z` derives that z once, from the generated results, +so the emitter and the validator cannot disagree. Emission descends to it, but +only once the nozzle is parked over the tower: descending while still over the +model would drive the nozzle into the print, so a descent that would do that is +deferred until after the travel to the tower. Extrusions emitted without an +explicit z — the nozzle-change wipe in particular — are pulled down to the +compacted z for the same reason. + +Reaching down is only safe if nothing tall stands near the tower. `Print.hpp` +carries the clearance rule: a keep-out zone grown from the tower's footprint by +the spiral z-hop envelope, and a per-object limit on how high an object may rise +near it, tiered by the nozzle cone, the head body, the rod and the lid. The same +rule serves the precise check on real extrusions, the pre-slice estimate that +feeds the plater, and the outlines the plater draws while an object is dragged, +so that the ring the user sees touches the object's outline exactly when the +check trips. + +## Merging sparse layers + +With `wipe_tower_sparse_layers_combination`, no layer is dropped and nothing is +compacted: the tower keeps following the object, and the nozzle never descends. +Instead a run of consecutive sparse layers prints once, on the run's last layer, +at the accumulated height of everything it covers — the same way infill +combination merges sparse infill. The layers below it in the run print nothing. + +`combine_sparse_wipe_tower_plan` runs before the tower's depths are planned, +because the heights it rewrites feed the extrusion flow of every later pass. It +raises `height` in place on the layer that prints a run and sets `combined_away` +on the rest; generation then proceeds unchanged, and the flag is copied onto the +results so the emitter can drop them. + +Four constraints shape the rule: + +- **Whole layers only.** A tower layer is entered at the object's z, so a merged + layer has to end on an object layer boundary. The merged height is therefore a + sum of whole layer heights, never a clamped value. +- **The nozzle's maximum layer height.** A run stops growing as soon as one more + layer would pass `max_layer_height` for the nozzle printing it — three quarters + of the nozzle diameter when that is left at 0, as elsewhere in slicing. The cap + is read through the filament-to-nozzle map, since `max_layer_height` is per + nozzle while the tower indexes filaments. This is what makes the setting inert + at common layer heights: two 0.2 mm layers are 0.4 mm and do not fit under a + 0.3 mm maximum, so nothing merges until the layer height is 0.15 mm or below, + or the maximum is raised. +- **A filament change purges at its own z.** A layer with a real change can + neither be merged away nor absorb the run below it, so a run always ends on its + own last sparse layer and the change above it is untouched. +- **The first layer stays on the bed.** It carries the brim and is never merged. + +A run holds one filament throughout — that is what makes it sparse — so the cap +is uniform across it, and the tower reserves depth only for the purges above a +layer, so a run has one footprint and the merged layer covers exactly the area +the layers it replaces would have. + +## Emission and accounting + +`WipeTowerIntegration` drops a layer whose results are marked, for both settings, +through the same `ignore_sparse` path in `tool_change` and +`is_empty_wipe_tower_gcode`. A dropped layer emits no travel to the tower and no +extrusion. + +Filament used is accumulated by the generators while they write, so a layer that +will be dropped must not be charged. Type 1 asks `layer_is_printed` at each of +its accumulation points; Type 2 guards the equivalent block in `finish_layer`, +which also stops a merged-away layer from adding height of its own — the layer +that prints the run carries all of it. + +A merged layer is the only case where the tower's layer height differs from the +object layer it sits on, and therefore the only case where the height the +exporter already emitted for that layer is wrong for the tower. Both generators +do declare a height, but each hardcodes a tag dialect — the block tower forces +the BBL tag, the other writes the compatible one — while the G-code processor +reads only the tag its printer uses. On a non-BBL printer with a Type 1 tower the +declaration is dropped, and the merged layer is drawn and costed as a thin one. +`WipeTowerIntegration::tower_height_tag` therefore declares it at export time, +where the printer is known, and only when the tower's own G-code does not already +carry the tag that will be read. The object's height returns on the next object +path, because emission forces the processor role to the tower on any layer that +carries one. + +## Constraints + +A layer that prints nothing prints nothing at all, including any interface work +the tower planner scheduled there. The Type 1 block planner marks a layer as a +contact layer when a filament category stops or starts being used relative to the +layer below, and a sparse layer immediately above a change qualifies. Merging a +run, like dropping its layers, replaces that interface with the run's single +layer. Both settings are off by default for this among other reasons. + +Neither setting changes what the tower is for. A plate that needs a tower on +every layer — smooth timelapse, clumping detection — gets one, and the settings +read as off rather than compacting or merging in one place and not another. + +## Implementation and verification + +- [WipeTower.hpp](../../src/libslic3r/GCode/WipeTower.hpp) declares the shared + rules and the plan-merging template; + [WipeTower.cpp](../../src/libslic3r/GCode/WipeTower.cpp) implements them and + the Type 1 tower, [WipeTower2.cpp](../../src/libslic3r/GCode/WipeTower2.cpp) + the Type 2 tower. +- [ToolOrdering.cpp](../../src/libslic3r/GCode/ToolOrdering.cpp) decides which + layers carry a tower at all, and + [Print.cpp](../../src/libslic3r/Print.cpp) plans it and runs the clearance + check whose rule lives in [Print.hpp](../../src/libslic3r/Print.hpp). +- [GCode.cpp](../../src/libslic3r/GCode.cpp) emits the tower, drops the layers + that print nothing, and declares a merged layer's height; + [PrintConfig.cpp](../../src/libslic3r/PrintConfig.cpp) defines the settings and + [ConfigManipulation.cpp](../../src/slic3r/GUI/ConfigManipulation.cpp) their + mutual exclusion. +- [GLCanvas3D.cpp](../../src/slic3r/GUI/GLCanvas3D.cpp) and + [PartPlate.cpp](../../src/slic3r/GUI/PartPlate.cpp) draw the compacted tower's + keep-out outlines live while the user drags. +- [Rule tests](../../tests/libslic3r/test_wipe_tower.cpp) cover the gating of + both settings, the per-layer predicates, the compacted z, the merge rule's run + flushing, height conservation, the nozzle cap and the first-layer exemption, + and the clearance geometry the plater draws. +- [Slicing tests](../../tests/fff_print/test_wipe_tower.cpp) slice a real print + and check that a run folds, that the tower still covers the object exactly + once, that a run too thin for the cap is left alone, and that a merged layer + declares its height in the tag the printer's processor reads. diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 40743dd5ca..c377b17860 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -970,6 +970,27 @@ static std::vector get_path_of_change_filament(const Print& print) return gcode; } + // A folded tower layer is thicker than the object layer it sits on, so the height process_layer + // emitted is not the tower's. Both writers declare one, but each hardcodes a tag dialect - Type 1 + // forces s_IsBBLPrinter and writes "; LAYER_HEIGHT:", Type 2 writes ";HEIGHT:" - and the processor + // reads only its printer's, so a Type 1 tower on a non-BBL printer loses it and the merged layer + // is drawn and costed as a thin one. Declare it here, where the printer is known, unless the tower + // already wrote the right tag. _extrude puts the object's height back on the next object path, + // since process_layer forces the role to erWipeTower on any layer with a tower. + std::string WipeTowerIntegration::tower_height_tag(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, + const std::string &tcr_gcode) const + { + const std::string tag = ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height); + if (! m_sparse_layers_combined || std::abs(gcodegen.m_last_height - tcr.layer_height) <= EPSILON || + tcr_gcode.find(tag) != std::string::npos) + return {}; + // Keep m_last_height what the G-code last declared, so a second visit does not repeat it. + gcodegen.m_last_height = tcr.layer_height; + char buf[64]; + sprintf(buf, "%s%g\n", tag.c_str(), tcr.layer_height); + return buf; + } + std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const { if (new_filament_id != -1 && new_filament_id != tcr.new_tool) @@ -1467,6 +1488,7 @@ static std::vector get_path_of_change_filament(const Print& print) config.set_key_value("filament_start_gcode", new ConfigOptionString(start_filament_gcode_str)); std::string tcr_gcode, tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_filament_id, &config); unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode); + gcode += tower_height_tag(gcodegen, tcr, tcr_gcode); gcode += tcr_gcode; // Count the toolchange only when the emitted block really changed the tool — // tower visits without a filament change must not advance the ordinal. @@ -1799,6 +1821,7 @@ static std::vector get_path_of_change_filament(const Print& print) std::string tcr_gcode, tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_extruder_id, &config); unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode); + gcode += tower_height_tag(gcodegen, tcr, tcr_gcode); gcode += tcr_gcode; check_add_eol(toolchange_gcode_str); @@ -1946,7 +1969,8 @@ static std::vector get_path_of_change_filament(const Print& print) // Calculate where the wipe tower layer will be printed. -1 means that print z will not change, // resulting in a wipe tower with sparse layers. double wipe_tower_z = -1; - bool ignore_sparse = false; + // Folded into a later, thicker layer that prints at its own z: nothing to emit. + bool ignore_sparse = wipe_tower_layer_is_combined_away(m_tool_changes[m_layer_idx]); if (m_sparse_layers_skipped) { wipe_tower_z = m_last_wipe_tower_print_z; ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]) && m_layer_idx != 0; @@ -1964,7 +1988,8 @@ static std::vector get_path_of_change_filament(const Print& print) // Calculate where the wipe tower layer will be printed. -1 means that print z will not change, // resulting in a wipe tower with sparse layers. double wipe_tower_z = -1; - bool ignore_sparse = false; + // Folded into a later, thicker layer that prints at its own z: nothing to emit. + bool ignore_sparse = wipe_tower_layer_is_combined_away(m_tool_changes[m_layer_idx]); if (m_sparse_layers_skipped) { ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]); wipe_tower_z = m_compacted_tower_z[m_layer_idx]; @@ -1994,7 +2019,7 @@ static std::vector get_path_of_change_filament(const Print& print) if (m_layer_idx >= (int) m_tool_changes.size()) return true; - bool ignore_sparse = false; + bool ignore_sparse = wipe_tower_layer_is_combined_away(m_tool_changes[m_layer_idx]); if (m_sparse_layers_skipped) ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 5208c4a5f3..41986cc1d6 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -107,7 +107,8 @@ public: m_is_first_print(true), m_print_config(&print_config), m_last_wipe_tower_print_z(print_config.z_offset.value), - m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(print_config)) + m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(print_config)), + m_sparse_layers_combined(wipe_tower_sparse_layers_combined(print_config)) { // Precomputed rather than accumulated while emitting, so that the clearance validator and // the emitter cannot disagree about where the compacted tower sits on any given layer. @@ -138,6 +139,7 @@ public: private: WipeTowerIntegration& operator=(const WipeTowerIntegration&); std::string append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const; + std::string tower_height_tag(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, const std::string &tcr_gcode) const; Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const Polygons &bed_polygons) const; std::string append_tcr2(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const; std::string travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const; @@ -175,6 +177,9 @@ private: // wipe_tower_no_sparse_layers, as answered by the shared compaction rule rather than by the raw // option: smooth timelapse and wrapping detection keep a tower on every layer regardless. const bool m_sparse_layers_skipped; + // Combined tower layers are thicker than the object layer they sit on, the only case where the + // tower's height is not the one process_layer already declared. + const bool m_sparse_layers_combined; // Print z of the compacted tower per planned layer. Empty when the tower is not compacted. std::vector m_compacted_tower_z; }; diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index eaf8a2eaf3..a700ada09f 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -49,6 +49,48 @@ std::vector compute_compacted_wipe_tower_z(const std::vector &layer_tool_changes) +{ + return ! layer_tool_changes.empty() && layer_tool_changes.front().combined_away; +} + +std::vector combine_sparse_wipe_tower_layers(std::vector &layer_height, + const std::vector &layer_is_sparse, + const std::vector &max_layer_height, + size_t first_layer_idx) +{ + assert(layer_is_sparse.size() == layer_height.size() && max_layer_height.size() == layer_height.size()); + std::vector combined_away(layer_height.size(), 0); + float pending_height = 0.f; // what the layers folded away so far add up to + for (size_t i = 0; i < layer_height.size(); ++i) { + // A toolchange has to purge at its own z, so it neither folds away nor takes over the run + // below it - and a run always flushes on its own last layer, so nothing is ever pending here. + if (! layer_is_sparse[i] || i <= first_layer_idx) { + pending_height = 0.f; + continue; + } + const float merged = pending_height + layer_height[i]; + // Hand the run on only if the next layer can swallow the whole thing; a layer already past + // the cap is left alone rather than shrunk. + const bool next_takes_it = i + 1 < layer_height.size() && layer_is_sparse[i + 1] && + merged + layer_height[i + 1] <= max_layer_height[i + 1] + float(EPSILON); + if (next_takes_it) { + combined_away[i] = 1; + pending_height = merged; + } else { + layer_height[i] = merged; + pending_height = 0.f; + } + } + return combined_away; +} + inline float align_round(float value, float base) { return std::round(value / base) * base; @@ -1904,6 +1946,7 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi //m_bridging(float(config.wipe_tower_bridging)), m_bridging(10.f), m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)), + m_sparse_layers_combined(wipe_tower_sparse_layers_combined(config)), m_gcode_flavor(config.gcode_flavor), m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))), m_current_tool(initial_tool), @@ -2028,6 +2071,16 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config) float nozzle_diameter = float(config.nozzle_diameter.get_at(idx)); m_filpar[idx].nozzle_diameter = nozzle_diameter; // to be used in future with (non-single) multiextruder MM + // Orca: max_layer_height is per nozzle, so read it through the filament->nozzle map rather than + // by filament id. Zero means three quarters of the nozzle diameter, as in Slicing.cpp. + { + const std::vector &filament_map = config.filament_map.values; // 1 based nozzle indices + const size_t nozzle_idx = idx < filament_map.size() && filament_map[idx] > 0 ? size_t(filament_map[idx] - 1) : 0; + const float max_layer_height = float(config.max_layer_height.get_at(nozzle_idx)); + m_filpar[idx].max_layer_height = max_layer_height > 0.f ? max_layer_height + : 0.75f * float(config.nozzle_diameter.get_at(nozzle_idx)); + } + float max_vol_speed = float(config.filament_max_volumetric_speed.get_at(idx)); if (max_vol_speed!= 0.f) m_filpar[idx].max_e_speed = (max_vol_speed / filament_area()); @@ -3001,7 +3054,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer(bool extrude_perimeter, bool // Ask our writer about how much material was consumed. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled. - if (! m_sparse_layers_skipped || toolchanges_on_layer) + if (layer_is_printed(toolchanges_on_layer)) if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); @@ -3898,7 +3951,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer_new(bool extrude_perimeter, // Ask our writer about how much material was consumed. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled. - if (!m_sparse_layers_skipped || toolchanges_on_layer) + if (layer_is_printed(toolchanges_on_layer)) if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); @@ -4008,7 +4061,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block(const WipeTowerBlock &block, // Ask our writer about how much material was consumed. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled. - if (!m_sparse_layers_skipped || toolchanges_on_layer) + if (layer_is_printed(toolchanges_on_layer)) if (filament_id < m_used_filament_length.size()) m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length(); @@ -4125,7 +4178,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block_solid(const WipeTowerBlock & // Ask our writer about how much material was consumed. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled. - if (!m_sparse_layers_skipped || toolchanges_on_layer) + if (layer_is_printed(toolchanges_on_layer)) if (filament_id < m_used_filament_length.size()) m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length(); @@ -4668,6 +4721,15 @@ void WipeTower::calc_block_infill_gap() m_extra_spacing = 1.f; } +// A folded layer is generated like any other but thrown away by the emitter, so its extrusions must +// not be charged to the filament used. +bool WipeTower::layer_is_printed(bool toolchanges_on_layer) const +{ + if (m_layer_info != m_plan.end() && m_layer_info->combined_away) + return false; + return ! m_sparse_layers_skipped || toolchanges_on_layer; +} + void WipeTower::plan_tower_new() { if (m_wipe_tower_brim_width < 0) m_wipe_tower_brim_width = get_auto_brim_by_height(m_wipe_tower_height); @@ -4820,6 +4882,9 @@ void WipeTower::generate_new(std::vector tool_changes; @@ -700,6 +711,62 @@ std::vector compute_compacted_wipe_tower_z(const std::vector &layer_tool_changes); + +// Folds runs of sparse layers into one. layer_height is raised in place on the layer that prints a +// run - always its last, so the merged extrusion lands on top of what it covers - and the returned +// mask marks the layers that now print nothing. A run stops growing once one more layer would pass +// max_layer_height of the nozzle that prints it. first_layer_idx and below never combine: the +// tower's first layer carries the brim. +std::vector combine_sparse_wipe_tower_layers(std::vector &layer_height, + const std::vector &layer_is_sparse, + const std::vector &max_layer_height, + size_t first_layer_idx); + +// Applies the rule above to a planned tower. Either generator's plan fits: both carry height, +// tool_changes and combined_away per layer, and index their filament parameters by tool. +template +void combine_sparse_wipe_tower_plan(PlanLayers &plan, const FilamentParams &filpar, size_t first_layer_idx, size_t initial_tool) +{ + const size_t n = plan.size(); + std::vector heights(n); + std::vector sparse(n); + std::vector caps(n); + + // A layer with no toolchange prints with the filament the layer below left loaded. + size_t tool = initial_tool; + for (const auto &layer : plan) + if (! layer.tool_changes.empty()) { + tool = layer.tool_changes.front().old_tool; + break; + } + for (size_t i = 0; i < n; ++i) { + heights[i] = plan[i].height; + sparse[i] = plan[i].tool_changes.empty() ? 1 : 0; + caps[i] = tool < filpar.size() ? filpar[tool].max_layer_height : 0.f; + if (! plan[i].tool_changes.empty()) + tool = plan[i].tool_changes.back().new_tool; + } + + const std::vector combined_away = combine_sparse_wipe_tower_layers(heights, sparse, caps, first_layer_idx); + for (size_t i = 0; i < n; ++i) { + plan[i].height = heights[i]; + plan[i].combined_away = combined_away[i] != 0; + } +} + + } // namespace Slic3r #endif // WipeTowerPrusaMM_hpp_ diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index 0080094270..a576172a39 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -1033,6 +1033,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau m_z_pos(0.f), m_bridging(float(config.wipe_tower_bridging)), m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)), + m_sparse_layers_combined(wipe_tower_sparse_layers_combined(config)), m_gcode_flavor(config.gcode_flavor), m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))), m_infill_speed(default_region_config.sparse_infill_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))), @@ -1150,6 +1151,16 @@ void WipeTower2::set_extruder(size_t idx, const PrintConfig& config) float nozzle_diameter = float(config.nozzle_diameter.get_at(idx)); m_filpar[idx].nozzle_diameter = nozzle_diameter; // to be used in future with (non-single) multiextruder MM + // Orca: max_layer_height is per nozzle, so read it through the filament->nozzle map rather than + // by filament id. Zero means three quarters of the nozzle diameter, as in Slicing.cpp. + { + const std::vector &filament_map = config.filament_map.values; // 1 based nozzle indices + const size_t nozzle_idx = idx < filament_map.size() && filament_map[idx] > 0 ? size_t(filament_map[idx] - 1) : 0; + const float max_layer_height = float(config.max_layer_height.get_at(nozzle_idx)); + m_filpar[idx].max_layer_height = max_layer_height > 0.f ? max_layer_height + : 0.75f * float(config.nozzle_diameter.get_at(nozzle_idx)); + } + float max_vol_speed = float(config.filament_max_volumetric_speed.get_at(idx)); if (max_vol_speed!= 0.f) m_filpar[idx].max_e_speed = (max_vol_speed / filament_area()); @@ -2103,7 +2114,9 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer() // Ask our writer about how much material was consumed. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled. - if (! m_sparse_layers_skipped || toolchanges_on_layer || first_layer) { + // A folded layer prints nothing, so it consumes nothing and adds no height of its own. + const bool combined_away = m_layer_info != m_plan.end() && m_layer_info->combined_away; + if ((! m_sparse_layers_skipped || toolchanges_on_layer || first_layer) && ! combined_away) { if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); m_current_height += m_layer_info->height; @@ -2435,6 +2448,10 @@ void WipeTower2::generate(std::vector> if (m_plan.empty()) return; + // Before planning: the layer heights this rewrites feed the extrusion flow of every later pass. + if (m_sparse_layers_combined) + combine_sparse_wipe_tower_plan(m_plan, m_filpar, m_first_layer_idx, m_current_tool); + plan_tower(); #if 1 for (int i=0;i<5;++i) { @@ -2533,6 +2550,10 @@ void WipeTower2::generate(std::vector> layer_result[idx] = merge_tcr(layer_result[idx], finish_layer_tcr); } + if (layer.combined_away) + for (WipeTower::ToolChangeResult &tcr : layer_result) + tcr.combined_away = true; + result.emplace_back(std::move(layer_result)); if (m_used_filament_length_until_layer.empty() || m_used_filament_length_until_layer.back().first != layer.z) diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index eb3a2562fb..02ce15672b 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -200,6 +200,8 @@ public: float tower_interface_pre_extrusion_length = 0.f; float tower_ironing_area = 4.f; float tower_interface_purge_length = 0.f; + // Tallest layer this filament's nozzle can lay down; caps the sparse layer combination. + float max_layer_height = 0.f; }; private: @@ -268,6 +270,7 @@ private: float m_extra_loading_move = 0.f; float m_bridging = 0.f; bool m_sparse_layers_skipped = false; + bool m_sparse_layers_combined = false; bool m_set_extruder_trimpot = false; bool m_adhesion = true; GCodeFlavor m_gcode_flavor; @@ -368,6 +371,8 @@ private: float z; // z position of the layer float height; // layer height float depth; // depth of the layer based on all layers above + // Folded into a later, thicker layer, so this one prints nothing at all. + bool combined_away{false}; float toolchanges_depth() const { float sum = 0.f; for (const auto &a : tool_changes) sum += a.required_depth; return sum; } std::vector tool_changes; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 214ef229df..9af8969490 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1199,6 +1199,7 @@ static std::vector s_Preset_print_options{ "enable_tower_interface_features", "enable_tower_interface_cooldown_during_tower", "wipe_tower_no_sparse_layers", + "wipe_tower_sparse_layers_combination", "compatible_printers", "compatible_printers_condition", "inherits", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 477c2c74ca..00398f9ebb 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -378,6 +378,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "wipe_tower_bridging" || opt_key == "wipe_tower_extra_flow" || opt_key == "wipe_tower_no_sparse_layers" + || opt_key == "wipe_tower_sparse_layers_combination" || opt_key == "flush_volumes_matrix" || opt_key == "prime_volume" || opt_key == "flush_into_infill" diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 9f317d21af..1d994d7874 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6705,6 +6705,20 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); + def = this->add("wipe_tower_sparse_layers_combination", coBool); + def->label = L("Combine sparse layers"); + def->tooltip = L("If enabled, consecutive layers on which the prime tower has no filament change are printed as a single " + "thicker tower layer instead of one thin layer each, the same way infill combination merges sparse infill. " + "The merged layer is printed at the top of the run, at the height of everything it covers.\n\n" + "Only whole layers are merged, and never past the maximum layer height of the nozzle printing the tower " + "(three quarters of the nozzle diameter when that is left at 0). Two or more layers therefore have to fit " + "under that limit before anything changes at all: at a 0.2 mm layer height under a 0.3 mm maximum nothing " + "is merged, while at 0.1 mm three layers become one.\n\n" + "Unlike \"No sparse layers\" the tower keeps following the model, so the toolhead never has to reach down to it. " + "Has no effect with \"No sparse layers\", smooth timelapse or clumping detection, which need a tower on every layer."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("single_extruder_multi_material_priming", coBool); def->label = L("Prime all printing extruders"); def->tooltip = L("If enabled, all printing extruders will be primed at the front edge of the print bed at the start of the print."); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 5d0ce03357..679811d699 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1632,6 +1632,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionString, toolchange_cyclic_order)) ((ConfigOptionBool, toolchange_cyclic_first_layer)) ((ConfigOptionBool, wipe_tower_no_sparse_layers)) + ((ConfigOptionBool, wipe_tower_sparse_layers_combination)) ((ConfigOptionString, change_filament_gcode)) ((ConfigOptionString, change_extrusion_role_gcode)) ((ConfigOptionString, process_change_extrusion_role_gcode)) diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index f40c71ec4a..5a58ec7b6c 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -1046,6 +1046,8 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in // Orca: both tower generators skip sparse layers, so this is not a wipe tower 2 exclusive. toggle_line("wipe_tower_no_sparse_layers", have_prime_tower); + // Dropping the sparse layers outright leaves nothing to combine, so the two are exclusive. + toggle_line("wipe_tower_sparse_layers_combination", have_prime_tower && !config->opt_bool("wipe_tower_no_sparse_layers")); WipeTowerWallType wipe_tower_wall_type = config->opt_enum("wipe_tower_wall_type"); bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 09843cbc94..bf348f856f 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3069,6 +3069,7 @@ void TabPrint::build() optgroup->append_single_option_line("wipe_tower_rib_width", "multimaterial_settings_prime_tower#rib-width"); optgroup->append_single_option_line("wipe_tower_fillet_wall", "multimaterial_settings_prime_tower#fillet-wall"); optgroup->append_single_option_line("wipe_tower_no_sparse_layers", "multimaterial_settings_prime_tower#no-sparse-layers"); + optgroup->append_single_option_line("wipe_tower_sparse_layers_combination", "multimaterial_settings_prime_tower#combine-sparse-layers"); optgroup->append_single_option_line("single_extruder_multi_material_priming", "multimaterial_settings_prime_tower"); optgroup = page->new_optgroup(L("Filament for Features"), L"param_filament_for_features"); diff --git a/tests/fff_print/test_wipe_tower.cpp b/tests/fff_print/test_wipe_tower.cpp index 24d50d6e66..014eab6f8b 100644 --- a/tests/fff_print/test_wipe_tower.cpp +++ b/tests/fff_print/test_wipe_tower.cpp @@ -308,6 +308,119 @@ TEST_CASE("A single-filament plate reserves a tower only when one is actually pr } } +// Filament 2 on the top surface only, so every layer below it is a toolchange-free tower layer: the +// run "Combine sparse layers" folds. The two heights decide whether anything folds, so they are the +// caller's business. +static DynamicPrintConfig sparse_run_config(double layer_height, const char *max_layer_height, bool combine) +{ + DynamicPrintConfig config = multifilament_config(2, { + { "top_surface_filament_id", 2 }, + { "enable_prime_tower", true }, + { "wipe_tower_x", 50 }, // inside the 200x200 test bed + { "wipe_tower_y", 50 }, + { "prime_tower_width", 35 }, + { "min_layer_height", "0.08"}, + { "single_extruder_multi_material", true }, + { "timelapse_type", "0" }, + { "enable_wrapping_detection", false }, + { "raft_layers", "0" } }); + // A taller first layer would top the plan and hide what the run does, so slice at one height. + config.set_deserialize_strict({ { "layer_height", std::to_string(layer_height) }, + { "initial_layer_print_height", std::to_string(layer_height) }, + { "max_layer_height", max_layer_height }, + { "wipe_tower_sparse_layers_combination", combine ? "1" : "0" } }); + return config; +} + +// What a sliced tower did with its sparse run. +struct SparseRunResult { size_t planned, sparse, folded; float tallest_printed, printed_height; std::string gcode; }; + +static SparseRunResult slice_sparse_run(const DynamicPrintConfig &config) +{ + Print print; + Model model; + init_print({ cube(10) }, print, model, config); + print.apply(model, config); + print.process(); + REQUIRE(print.is_step_done(psWipeTower)); + + SparseRunResult r{}; + for (const std::vector &layer : print.wipe_tower_data().tool_changes) { + if (layer.empty()) + continue; + ++r.planned; + if (wipe_tower_layer_is_sparse(layer)) + ++r.sparse; + if (wipe_tower_layer_is_combined_away(layer)) { + ++r.folded; + } else { + r.tallest_printed = std::max(r.tallest_printed, layer.front().layer_height); + r.printed_height += layer.front().layer_height; + } + } + r.gcode = Slic3r::Test::gcode(print); + return r; +} + +// How often the G-code declares `height` in the tag this printer's processor reads. The dialect is a +// global the exporter sets from the printer, so this is only correct after a slice - the point below. +static size_t count_height_tags(const std::string &gcode, const char *height) +{ + const std::string tag = ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + height + "\n"; + size_t n = 0; + for (size_t p = gcode.find(tag); p != std::string::npos; p = gcode.find(tag, p + 1)) + ++n; + return n; +} + +TEST_CASE("Combining sparse layers folds a run into whole layers the nozzle can lay down", "[WipeTower]") +{ + // 0.1 mm layers under a 0.32 mm cap: three fit (0.3), a fourth does not, so a run prints once + // every three layers at 0.3 mm. + const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.1, "0.32", false)); + const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.1, "0.32", true)); + + REQUIRE(plain.planned == combined.planned); // the plan still has one layer per object layer + REQUIRE(plain.sparse > 10); + CHECK(plain.folded == 0); + CHECK_THAT(plain.tallest_printed, Catch::Matchers::WithinAbs(0.1f, 1e-4f)); + + CHECK(combined.folded > 0); + CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.3f, 1e-4f)); + // Two of every three sparse layers fold away, leaving the toolchange layers untouched. + CHECK(combined.folded <= plain.sparse); + CHECK(combined.folded >= plain.sparse / 2); + // What folds away comes back as height on the layer that prints the run: no gap, nothing twice. + CHECK_THAT(combined.printed_height, Catch::Matchers::WithinAbs(plain.printed_height, 1e-3f)); +} + +TEST_CASE("A run too thin to reach the nozzle's layer height is left alone", "[WipeTower]") +{ + // Only whole layers merge, so two 0.2 mm layers (0.4) do not fit a 0.32 mm maximum and the tower + // prints as if the option were off. This is the common 0.4 nozzle case; the tooltip says so. + const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.2, "0.32", false)); + const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.2, "0.32", true)); + + REQUIRE(plain.sparse > 10); + CHECK(combined.folded == 0); + CHECK(combined.planned == plain.planned); + CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.2f, 1e-4f)); +} + +TEST_CASE("A merged tower layer declares its own height to the G-code processor", "[WipeTower]") +{ + // Each writer declares a height in a hardcoded tag dialect while the processor reads only its + // printer's, so one of them is always dropped. A merged layer is the first time that shows, as a + // thick layer drawn and costed as a thin one. 0.2 mm layers under a 0.42 mm maximum merge in pairs. + const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.2, "0.42", false)); + const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.2, "0.42", true)); + + REQUIRE(combined.folded > 0); + CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.4f, 1e-4f)); + // Every layer that prints a merged run has to say so, and nothing may say so without the option. + CHECK(count_height_tags(combined.gcode, "0.4") - count_height_tags(plain.gcode, "0.4") == combined.folded); +} + TEST_CASE("A tower printed without a tool change is still validated against the bed", "[WipeTower]") { // Wrapping detection prints a tower on a plate that purges one filament. Neither the old diff --git a/tests/libslic3r/test_wipe_tower.cpp b/tests/libslic3r/test_wipe_tower.cpp index 277e11eceb..6f6daa74b8 100644 --- a/tests/libslic3r/test_wipe_tower.cpp +++ b/tests/libslic3r/test_wipe_tower.cpp @@ -278,6 +278,98 @@ TEST_CASE("Only the keep-out ring an object is measured against is drawn", "[Wip CHECK_THAT(unscaled(get_extents(zone.grown_body).max.x()), WithinAbs(10. + 0.5 * (40. - 0.2), 0.02)); } +// --------------------------------------------------------------------------------------------- +// "Combine sparse layers": folding a run of toolchange-free layers into one thicker tower layer. +// --------------------------------------------------------------------------------------------- + +TEST_CASE("Sparse layers are combined only when every layer is still the tower's to place", "[WipeTower][CombineSparseLayers]") { + PrintConfig cfg; + cfg.timelapse_type.value = TimelapseType::tlTraditional; + cfg.enable_wrapping_detection.value = false; + cfg.wipe_tower_no_sparse_layers.value = false; + + cfg.wipe_tower_sparse_layers_combination.value = false; + CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg)); + cfg.wipe_tower_sparse_layers_combination.value = true; + CHECK(wipe_tower_sparse_layers_combined(cfg)); + + // Dropping the sparse layers outright leaves nothing to combine. + cfg.wipe_tower_no_sparse_layers.value = true; + CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg)); + CHECK(wipe_tower_sparse_layers_skipped(cfg)); + cfg.wipe_tower_no_sparse_layers.value = false; + + // Both of these park the nozzle on the tower every layer, so no layer may be folded away. + cfg.timelapse_type.value = TimelapseType::tlSmooth; + CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg)); + cfg.timelapse_type.value = TimelapseType::tlTraditional; + cfg.enable_wrapping_detection.value = true; + CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg)); +} + +TEST_CASE("A layer folded into a later one is marked on the results the emitter reads", "[WipeTower][CombineSparseLayers]") { + WipeTower::ToolChangeResult folded = make_tcr(1, 1, 0.2f); + folded.combined_away = true; + CHECK(wipe_tower_layer_is_combined_away({folded})); + CHECK_FALSE(wipe_tower_layer_is_combined_away({make_tcr(1, 1, 0.2f)})); + CHECK_FALSE(wipe_tower_layer_is_combined_away({})); +} + +TEST_CASE("A run of sparse layers prints once, on its last layer, at the height it covers", "[WipeTower][CombineSparseLayers]") { + // Eight 0.1 mm layers on a 0.3 mm cap: a toolchange on the first and the last, sparse between. + std::vector heights(8, 0.1f); + const std::vector sparse{0, 1, 1, 1, 1, 1, 1, 0}; + const std::vector caps(8, 0.3f); + + const std::vector combined = combine_sparse_wipe_tower_layers(heights, sparse, caps, 0); + REQUIRE(combined.size() == heights.size()); + // Three layers fill the cap exactly: the run flushes on layers 3 and 6, the two below each go. + CHECK(combined == std::vector{0, 1, 1, 0, 1, 1, 0, 0}); + CHECK_THAT(heights[3], WithinAbs(0.3f, 1e-5f)); + CHECK_THAT(heights[6], WithinAbs(0.3f, 1e-5f)); + // Layers that print keep the object covered: nothing is lost and nothing is printed twice. + float printed = 0.f; + for (size_t i = 0; i < heights.size(); ++i) + if (! combined[i]) + printed += heights[i]; + CHECK_THAT(printed, WithinAbs(0.8f, 1e-5f)); + // A toolchange has to purge at its own z, so those layers are left exactly as planned. + CHECK_THAT(heights[0], WithinAbs(0.1f, 1e-5f)); + CHECK_THAT(heights[7], WithinAbs(0.1f, 1e-5f)); +} + +TEST_CASE("The maximum layer height of the nozzle that prints the run caps the merge", "[WipeTower][CombineSparseLayers]") { + // The cap that counts belongs to the layer that prints the run; one that prints nothing lays + // nothing down, so its own cap cannot constrain it. Five 0.1 mm layers, sparse above the first, + // layer 3's nozzle taking only 0.15. (A real run holds one filament, so this only tests the + // look-ahead.) + std::vector heights(5, 0.1f); + std::vector caps(5, 0.3f); + caps[3] = 0.15f; + const std::vector combined = combine_sparse_wipe_tower_layers(heights, {0, 1, 1, 1, 1}, caps, 0); + // Layer 2 cannot hand its 0.2 mm on to layer 3, so it prints there and a fresh run starts above. + CHECK(combined == std::vector{0, 1, 0, 1, 0}); + CHECK_THAT(heights[2], WithinAbs(0.2f, 1e-5f)); + CHECK_THAT(heights[4], WithinAbs(0.2f, 1e-5f)); + + // A single layer already past the cap is printed as planned rather than shrunk. + std::vector tall{0.2f, 0.4f, 0.4f}; + const std::vector tall_combined = combine_sparse_wipe_tower_layers(tall, {0, 1, 1}, {0.3f, 0.3f, 0.3f}, 0); + CHECK(tall_combined == std::vector{0, 0, 0}); + CHECK_THAT(tall[1], WithinAbs(0.4f, 1e-5f)); +} + +TEST_CASE("The tower's first layer is never folded away", "[WipeTower][CombineSparseLayers]") { + // It carries the brim and has to sit on the bed, however little it purges. + std::vector heights(4, 0.1f); + const std::vector combined = combine_sparse_wipe_tower_layers(heights, {1, 1, 1, 1}, std::vector(4, 0.5f), 0); + CHECK(combined.front() == 0); + CHECK_THAT(heights.front(), WithinAbs(0.1f, 1e-5f)); + // Everything above it merges into the top layer, which the cap still fits. + CHECK(combined == std::vector{0, 1, 1, 0}); + CHECK_THAT(heights.back(), WithinAbs(0.3f, 1e-5f)); +} + TEST_CASE("Footprint padding covers the brim and the extrusion half width on each side", "[WipeTower][NoSparseLayers]") { // A nominal outline hulls extrusion centre lines and is re-centred once the real wall is known, // so a line width per side on top of the brim is what keeps an estimate enclosing the real tower.