mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Wipe tower sparse layers combination (#15841)
This commit is contained in:
@@ -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.
|
||||
+28
-3
@@ -970,6 +970,27 @@ static std::vector<Vec2d> 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<Vec2d> 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<Vec2d> 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<Vec2d> 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<Vec2d> 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<Vec2d> 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]);
|
||||
|
||||
|
||||
@@ -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<float> m_compacted_tower_z;
|
||||
};
|
||||
|
||||
@@ -49,6 +49,48 @@ std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<
|
||||
return tower_z;
|
||||
}
|
||||
|
||||
bool wipe_tower_sparse_layers_combined(const PrintConfig &config)
|
||||
{
|
||||
return config.wipe_tower_sparse_layers_combination.value && ! wipe_tower_sparse_layers_skipped(config) &&
|
||||
config.timelapse_type.value != TimelapseType::tlSmooth && ! config.enable_wrapping_detection.value;
|
||||
}
|
||||
|
||||
bool wipe_tower_layer_is_combined_away(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes)
|
||||
{
|
||||
return ! layer_tool_changes.empty() && layer_tool_changes.front().combined_away;
|
||||
}
|
||||
|
||||
std::vector<char> combine_sparse_wipe_tower_layers(std::vector<float> &layer_height,
|
||||
const std::vector<char> &layer_is_sparse,
|
||||
const std::vector<float> &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<char> 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<int> &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<std::vector<WipeTower::ToolChangeResult
|
||||
if (m_plan.empty())
|
||||
return;
|
||||
//m_extra_spacing = 1.f;
|
||||
// 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);
|
||||
m_wipe_tower_height = m_plan.back().z;//real wipe_tower_height
|
||||
plan_tower_new();
|
||||
m_layer_info = m_plan.begin();
|
||||
@@ -5014,6 +5079,9 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
|
||||
if (only_generate_wall && !timelapse_wall.gcode.empty()) {
|
||||
layer_result.insert(layer_result.begin(), std::move(timelapse_wall));
|
||||
}
|
||||
if (layer.combined_away)
|
||||
for (WipeTower::ToolChangeResult &tcr : layer_result)
|
||||
tcr.combined_away = true;
|
||||
result.emplace_back(std::move(layer_result));
|
||||
}
|
||||
assert(m_outer_wall.size() == m_plan.size());
|
||||
@@ -5179,7 +5247,7 @@ WipeTower::ToolChangeResult WipeTower::only_generate_out_wall(bool is_new_mode)
|
||||
|
||||
// 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();
|
||||
|
||||
return construct_tcr(writer, false, old_tool, true, false, 0.f, false);
|
||||
|
||||
@@ -152,6 +152,10 @@ public:
|
||||
bool is_contact = false;
|
||||
NozzleChangeResult nozzle_change_result;
|
||||
|
||||
// Orca: folded into a later, thicker layer, so the emitter drops it. Set by the tower, so
|
||||
// the two cannot disagree about which layers print.
|
||||
bool combined_away = false;
|
||||
|
||||
// Sum the total length of the extrusion.
|
||||
float total_extrusion_length_in_plane() {
|
||||
float e_length = 0.f;
|
||||
@@ -391,6 +395,8 @@ public:
|
||||
float filament_tower_interface_pre_extrusion_dist = 0;
|
||||
float filament_tower_interface_pre_extrusion_length = 0;
|
||||
float filament_petg_pre_extrusion_offset_dist = 0;
|
||||
// Tallest layer this filament's nozzle can lay down; caps the sparse layer combination.
|
||||
float max_layer_height = 0.f;
|
||||
};
|
||||
|
||||
|
||||
@@ -522,6 +528,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;
|
||||
// BBS: remove useless config
|
||||
//bool m_set_extruder_trimpot = false;
|
||||
bool m_adhesion = true;
|
||||
@@ -595,6 +602,8 @@ private:
|
||||
}
|
||||
// Calculates depth for all layers and propagates them downwards
|
||||
void plan_tower();
|
||||
// Whether the layer reaches the G-code, and so whether its extrusions count as filament used.
|
||||
bool layer_is_printed(bool toolchanges_on_layer) const;
|
||||
|
||||
// Goes through m_plan and recalculates depths and width of the WT to make it exactly square - experimental
|
||||
void make_wipe_tower_square();
|
||||
@@ -634,6 +643,8 @@ private:
|
||||
float depth; // depth of the layer based on all layers above
|
||||
float extra_spacing;
|
||||
bool extruder_fill{true};
|
||||
// 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<ToolChange> tool_changes;
|
||||
@@ -700,6 +711,62 @@ std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<
|
||||
float base_z = 0.f);
|
||||
|
||||
|
||||
// Combination rule for wipe_tower_sparse_layers_combination. Nothing is compacted - the tower keeps
|
||||
// following the object - but a run of consecutive toolchange-free layers prints as one thicker layer,
|
||||
// the way infill combination merges sparse infill. Shared so that neither tower generator nor the
|
||||
// G-code emitter can combine on its own.
|
||||
|
||||
// Whether sparse layers are really combined. Skipping them outright is the stronger answer to the
|
||||
// same problem and wins over this; smooth timelapse and wrapping detection need a tower on every
|
||||
// layer, so they rule it out too.
|
||||
bool wipe_tower_sparse_layers_combined(const PrintConfig &config);
|
||||
|
||||
// A planned layer folded into a later, thicker one prints nothing at all.
|
||||
bool wipe_tower_layer_is_combined_away(const std::vector<WipeTower::ToolChangeResult> &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<char> combine_sparse_wipe_tower_layers(std::vector<float> &layer_height,
|
||||
const std::vector<char> &layer_is_sparse,
|
||||
const std::vector<float> &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<class PlanLayers, class FilamentParams>
|
||||
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<float> heights(n);
|
||||
std::vector<char> sparse(n);
|
||||
std::vector<float> 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<char> 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_
|
||||
|
||||
@@ -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<int> &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<std::vector<WipeTower::ToolChangeResult>>
|
||||
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<std::vector<WipeTower::ToolChangeResult>>
|
||||
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)
|
||||
|
||||
@@ -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<ToolChange> tool_changes;
|
||||
|
||||
@@ -1199,6 +1199,7 @@ static std::vector<std::string> 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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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<WipeTowerWallType>("wipe_tower_wall_type");
|
||||
bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<WipeTower::ToolChangeResult> &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
|
||||
|
||||
@@ -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<float> heights(8, 0.1f);
|
||||
const std::vector<char> sparse{0, 1, 1, 1, 1, 1, 1, 0};
|
||||
const std::vector<float> caps(8, 0.3f);
|
||||
|
||||
const std::vector<char> 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<char>{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<float> heights(5, 0.1f);
|
||||
std::vector<float> caps(5, 0.3f);
|
||||
caps[3] = 0.15f;
|
||||
const std::vector<char> 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<char>{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<float> tall{0.2f, 0.4f, 0.4f};
|
||||
const std::vector<char> tall_combined = combine_sparse_wipe_tower_layers(tall, {0, 1, 1}, {0.3f, 0.3f, 0.3f}, 0);
|
||||
CHECK(tall_combined == std::vector<char>{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<float> heights(4, 0.1f);
|
||||
const std::vector<char> combined = combine_sparse_wipe_tower_layers(heights, {1, 1, 1, 1}, std::vector<float>(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<char>{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.
|
||||
|
||||
Reference in New Issue
Block a user