mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 22:42:37 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60ebbf7daa |
@@ -330,13 +330,12 @@ jobs:
|
||||
# sources are unchanged, so a re-run of the same commit would skip the
|
||||
# OrcaSlicer module and ship no test asset. A per-run value in that module's
|
||||
# env keeps it rebuilding; orca_deps stays cached, and the compiler cache
|
||||
# still serves the rebuild. run-tests on the same module builds the test
|
||||
# binaries.
|
||||
- name: Inject commit hash, run-tests and cache buster into Flatpak manifest
|
||||
# still serves the rebuild.
|
||||
- name: Inject commit hash and flatpak-builder cache buster into Flatpak manifest
|
||||
env:
|
||||
flatpak_builder_cache_buster: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
run: |
|
||||
sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n run-tests: true\n build-options:\n env:\n flatpak_builder_cache_buster: \"$flatpak_builder_cache_buster\"\n git_commit_hash: \"$git_commit_hash\"|}" \
|
||||
sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n build-options:\n env:\n flatpak_builder_cache_buster: \"$flatpak_builder_cache_buster\"\n git_commit_hash: \"$git_commit_hash\"|}" \
|
||||
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
|
||||
shell: bash
|
||||
# flatpak-builder's --ccache only wraps cc and gcc, and the manifest builds
|
||||
@@ -382,6 +381,9 @@ jobs:
|
||||
save-cache: false
|
||||
arch: ${{ matrix.variant.arch }}
|
||||
upload-artifact: false
|
||||
# run-tests fires the module's build-only test-commands; keep-build-dirs
|
||||
# retains the binaries for the packaging step below.
|
||||
run-tests: true
|
||||
keep-build-dirs: true
|
||||
# The build has just touched everything it can use, so an object untouched
|
||||
# for a week is dead, usually orphaned by a flag change.
|
||||
|
||||
@@ -16408,7 +16408,7 @@ msgid "Min print speed"
|
||||
msgstr "Minimale Druckgeschwindigkeit"
|
||||
|
||||
msgid "The minimum print speed to which the printer slows down to maintain the minimum layer time defined above when the slowdown for better layer cooling is enabled."
|
||||
msgstr "Die minimale Druckgeschwindigkeit, auf die der Drucker verlangsamt wird, um die oben definierte minimale Schichtzeit einzuhalten, wenn die Verlangsamung für eine bessere Schichtkühlung aktiviert ist."
|
||||
msgstr "Das minimale Drucktempo, bei dem der Drucker verlangsamt wird, um die oben definierte minimale Schichtzeit einzuhalten, wenn die Verlangsamung für eine bessere Schichtkühlung aktiviert ist."
|
||||
|
||||
msgid "The diameter of nozzle."
|
||||
msgstr "Düsendurchmesser"
|
||||
|
||||
@@ -384,7 +384,7 @@ modules:
|
||||
- cmake --build build_flatpak --target generate_system_cache -j$FLATPAK_BUILDER_N_JOBS
|
||||
- ./scripts/build_preset_cache.sh -n -b build_flatpak /app/share/OrcaSlicer/profiles
|
||||
|
||||
# Built (not run) here when CI injects run-tests, then shipped to a separate
|
||||
# Built (not run) here via the action's run-tests, then shipped to a separate
|
||||
# test job. Only the test sources compile; nothing installs to /app.
|
||||
test-commands:
|
||||
- cmake . -B build_flatpak -DBUILD_TESTS=ON
|
||||
|
||||
@@ -776,9 +776,7 @@ double ConfigBase::get_abs_value(const t_config_option_key &opt_key, double rati
|
||||
{
|
||||
// Get stored option value.
|
||||
const ConfigOption *raw_opt = this->option(opt_key);
|
||||
// Mirror the single-arg overload — assert() is a no-op under NDEBUG.
|
||||
if (raw_opt == nullptr)
|
||||
throw ConfigurationError("ConfigBase::get_abs_value(): \"" + opt_key + "\" is not defined");
|
||||
assert(raw_opt != nullptr);
|
||||
if (raw_opt->type() != coFloatOrPercent)
|
||||
throw ConfigurationError("ConfigBase::get_abs_value(): opt_key is not of coFloatOrPercent");
|
||||
// Compute absolute value.
|
||||
|
||||
@@ -107,6 +107,12 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeT
|
||||
// normalize_fdm_2 clearing enable_prime_tower. Its mixed-filament case is not modelled.
|
||||
const bool need_wipe_tower = smooth_timelapse || wrapping;
|
||||
|
||||
// Fewer than two filaments cannot make a tool change, so only wrapping detection or smooth
|
||||
// timelapse print a tower then. The flush volume is no proof of one: it is read from the
|
||||
// matrix of every configured slot, nonzero even when a single one of them is used.
|
||||
if (filaments_cnt < 2 && !need_wipe_tower)
|
||||
return footprint;
|
||||
|
||||
// A tower printed for one of the reasons above has no tool change to purge for; both
|
||||
// planners give it the idle depth below and nothing more.
|
||||
const size_t purge_count = filaments_cnt > 1 ? (dual_nozzle ? filaments_cnt : filaments_cnt - 1) : 0;
|
||||
@@ -151,14 +157,6 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeT
|
||||
purges[longest_ramming].filament_change_length = float(float_at("filament_change_length", filament_ids[longest_ramming], 0.) * double(nozzles.size() - 1));
|
||||
}
|
||||
|
||||
// Both wall types decide this together: over-reserving only wastes bed area, but reporting
|
||||
// no tower for one that is built collapses the validation hull to a point.
|
||||
// A tool change is a reason on its own (see the base commit); Type1 already reserves
|
||||
// per filament, Type2 has only the volume, which can resolve to zero.
|
||||
const bool has_purge = type1 ? !purges.empty() : volume > EPSILON;
|
||||
if (!has_purge && filaments_cnt < 2 && !need_wipe_tower)
|
||||
return footprint;
|
||||
|
||||
const double min_depth = WipeTower::get_limit_depth_by_height(float(max_object_height));
|
||||
const float perimeter_width = float(nozzle_diameter) * 1.25f; // Width_To_Nozzle_Ratio
|
||||
// With nothing to purge, plan_tower_new sizes the tower for wrapping detection or the
|
||||
@@ -171,7 +169,10 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeT
|
||||
if (!purges.empty())
|
||||
side = WipeTower::estimate_rib_tower_bbox_side(purges, float(width), float(layer_height), float(nozzle_diameter), float(extra_spacing), float(rib_width), float(extra_rib_length), float(max_object_height));
|
||||
else {
|
||||
const double square = has_purge ? std::sqrt(volume / layer_height * extra_spacing) : idle_depth;
|
||||
// Type2 squares the tower from its purge volume; Type1 with no purge list (a lone
|
||||
// filament kept for timelapse or wrapping) sizes for the idle depth.
|
||||
const bool has_purge = !type1 && volume > EPSILON;
|
||||
const double square = has_purge ? std::sqrt(volume / layer_height * extra_spacing) : idle_depth;
|
||||
side = WipeTower::rib_footprint_side(float(square), float(square), float(rib_width), float(extra_rib_length), float(max_object_height));
|
||||
}
|
||||
footprint.width = footprint.depth = side;
|
||||
|
||||
@@ -120,6 +120,9 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe
|
||||
fill_no_overlap
|
||||
);
|
||||
|
||||
if (this->layer()->lower_layer != nullptr)
|
||||
// Cummulative sum of polygons over all the regions.
|
||||
g.lower_slices = &this->layer()->lower_layer->lslices;
|
||||
if (this->layer()->upper_layer != NULL)
|
||||
g.upper_slices = &this->layer()->upper_layer->lslices;
|
||||
|
||||
@@ -132,13 +135,6 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe
|
||||
g.overhang_flow = this->bridging_flow(frPerimeter, object_config.thick_bridges);
|
||||
g.solid_infill_flow = this->flow(frSolidInfill);
|
||||
|
||||
// Cumulative sum of polygons over all the regions, less what the lower layer could not print.
|
||||
ExPolygons lower_slices;
|
||||
if (this->layer()->lower_layer != nullptr) {
|
||||
lower_slices = g.printable_slices(this->layer()->lower_layer->lslices);
|
||||
g.lower_slices = &lower_slices;
|
||||
}
|
||||
|
||||
if (this->layer()->object()->config().wall_generator.value == PerimeterGeneratorType::Arachne && !spiral_mode)
|
||||
g.process_arachne();
|
||||
else
|
||||
|
||||
@@ -2885,21 +2885,6 @@ bool PerimeterGeneratorLoop::is_internal_contour() const
|
||||
return true;
|
||||
}
|
||||
|
||||
// ORCA: Arachne drops features below min_feature_size, classic builds nothing thinner than a third of the
|
||||
// nozzle. Both describe the layer below, a union of regions sharing neither nozzle nor generator, so every
|
||||
// ambiguity resolves low: it may keep a sliver that was never printed, but it never drops one that was.
|
||||
ExPolygons PerimeterGenerator::printable_slices(const ExPolygons &slices) const
|
||||
{
|
||||
double min_width = *std::min_element(print_config->nozzle_diameter.values.begin(),
|
||||
print_config->nozzle_diameter.values.end()) / 3.;
|
||||
if (object_config->wall_generator.value == PerimeterGeneratorType::Arachne) {
|
||||
const double min_feature_size = Arachne::make_paths_params(layer_id, *object_config, *print_config).min_feature_size;
|
||||
// Spiral vase can put a classic layer under an Arachne one, so there both limits apply.
|
||||
min_width = print_config->spiral_mode ? std::min(min_width, min_feature_size) : min_feature_size;
|
||||
}
|
||||
return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices;
|
||||
}
|
||||
|
||||
std::vector<Polygons> PerimeterGenerator::generate_lower_polygons_series(float width)
|
||||
{
|
||||
float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1);
|
||||
|
||||
@@ -149,8 +149,6 @@ public:
|
||||
//BBS
|
||||
double smaller_width_ext_mm3_per_mm() const { return m_ext_mm3_per_mm_smaller_width; }
|
||||
Polygons lower_slices_polygons() const { return m_lower_slices_polygons; }
|
||||
// ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support.
|
||||
ExPolygons printable_slices(const ExPolygons &slices) const;
|
||||
|
||||
private:
|
||||
std::vector<Polygons> generate_lower_polygons_series(float width);
|
||||
|
||||
+3
-14
@@ -11,23 +11,12 @@ namespace Slic3r {
|
||||
float CalibPressureAdvance::find_optimal_PA_speed(const DynamicPrintConfig &config, double line_width, double layer_height, int extruder_id, int filament_idx)
|
||||
{
|
||||
const double general_suggested_min_speed = 100.0;
|
||||
// Read defensively — CLI callers may hand us a config missing optional keys.
|
||||
auto vector_at = [&config](const char *key, int idx) -> double {
|
||||
if (const auto *o = config.option<ConfigOptionFloats>(key)) return o->get_at(idx);
|
||||
const ConfigOptionDef *d = config.def() ? config.def()->get(key) : nullptr;
|
||||
return (d && d->default_value) ? d->get_default_value<ConfigOptionFloats>()->get_at(idx) : 0.0;
|
||||
};
|
||||
auto nullable_at = [&config](const char *key, int idx) -> double {
|
||||
if (const auto *o = config.option<ConfigOptionFloatsNullable>(key)) return o->get_at(idx);
|
||||
const ConfigOptionDef *d = config.def() ? config.def()->get(key) : nullptr;
|
||||
return (d && d->default_value) ? d->get_default_value<ConfigOptionFloatsNullable>()->get_at(idx) : 0.0;
|
||||
};
|
||||
double filament_max_volumetric_speed = vector_at("filament_max_volumetric_speed", filament_idx);
|
||||
double filament_max_volumetric_speed = config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->get_at(filament_idx);
|
||||
// todo multi_extruders:
|
||||
const float nozzle_diameter = vector_at("nozzle_diameter", extruder_id);
|
||||
const float nozzle_diameter = config.option<ConfigOptionFloats>("nozzle_diameter")->get_at(extruder_id);
|
||||
if (line_width <= 0.) line_width = Flow::auto_extrusion_width(frPerimeter, nozzle_diameter);
|
||||
Flow pattern_line = Flow(line_width, layer_height, nozzle_diameter);
|
||||
auto pa_speed = std::min(std::max(general_suggested_min_speed, nullable_at("outer_wall_speed", extruder_id)),
|
||||
auto pa_speed = std::min(std::max(general_suggested_min_speed, config.option<ConfigOptionFloatsNullable>("outer_wall_speed")->get_at(extruder_id)),
|
||||
filament_max_volumetric_speed / pattern_line.mm3_per_mm());
|
||||
|
||||
return std::floor(pa_speed);
|
||||
|
||||
@@ -234,10 +234,8 @@ bool GLGizmosManager::init()
|
||||
#ifdef SLIC3R_CAD
|
||||
// Registered last: Primitive and Sketch are the final entries before Undefined, so
|
||||
// omitting them leaves every preceding m_gizmos index (indexed by EType) untouched.
|
||||
if (wxGetApp().is_enable_cad_feature()) {
|
||||
m_gizmos.emplace_back(new GLGizmoPrimitive(m_parent, m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg", static_cast<unsigned int>(Primitive)));
|
||||
m_gizmos.emplace_back(new GLGizmoSketch(m_parent, m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg", static_cast<unsigned int>(Sketch)));
|
||||
}
|
||||
m_gizmos.emplace_back(new GLGizmoPrimitive(m_parent, m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg", static_cast<unsigned int>(Primitive)));
|
||||
m_gizmos.emplace_back(new GLGizmoSketch(m_parent, m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg", static_cast<unsigned int>(Sketch)));
|
||||
#endif
|
||||
//m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", sprite_id++));
|
||||
//m_gizmos.emplace_back(new GLGizmoFaceDetector(m_parent, "face recognition.svg", sprite_id++));
|
||||
@@ -1346,8 +1344,7 @@ GLGizmoBase* GLGizmosManager::get_current() const
|
||||
|
||||
GLGizmoBase* GLGizmosManager::get_gizmo(GLGizmosManager::EType type) const
|
||||
{
|
||||
// m_gizmos ends before the enum does when the CAD gizmos are not registered.
|
||||
return type < m_gizmos.size() ? m_gizmos[type].get() : nullptr;
|
||||
return ((type == Undefined) || m_gizmos.empty()) ? nullptr : m_gizmos[type].get();
|
||||
}
|
||||
|
||||
GLGizmosManager::EType GLGizmosManager::get_gizmo_from_name(const std::string& gizmo_name) const
|
||||
|
||||
@@ -495,138 +495,3 @@ TEST_CASE("Loops waiting for the infill are extruded after it", "[Perimeters]")
|
||||
CHECK(ceiling_roles(true) == std::vector<std::string>{ "perimeter", "infill", "perimeter" });
|
||||
CHECK(ceiling_roles(false) == std::vector<std::string>{ "perimeter", "infill" });
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// The rib spans z=[0,5] and the slab z=[5,6], so this is the slab's first layer - the only one whose
|
||||
// support comes from the rib rather than from the slab below it.
|
||||
const double slab_first_layer_z = 5.2;
|
||||
|
||||
// Rib widths either side of what the wall generators can print. At a 0.4mm nozzle the classic generator
|
||||
// builds nothing thinner than nozzle/3 = 0.133mm and Arachne drops anything below min_feature_size, 25%
|
||||
// of the nozzle = 0.1mm. 0.08mm is under both thresholds, 0.3mm over both.
|
||||
const double unprintable_rib = 0.08;
|
||||
const double printable_rib = 0.3;
|
||||
|
||||
// A 4x5mm anchor tower carrying a 20x5mm slab at z=[5,6], with a rib `rib_width` wide running the whole
|
||||
// length of the slab beneath its y=0 edge; a `rib_width` of 0 leaves the rib out. Nothing else is under
|
||||
// that edge, so whether the wall along it is an overhang rests entirely on the rib. Overhang detection
|
||||
// grows the lower slices by half the nozzle diameter before it asks, which carries either rib past the
|
||||
// 0.21mm from the slab edge to that wall - the unprintable one only fails to reach it once it is filtered
|
||||
// out for being unprintable.
|
||||
Print &slab_over_rib(Print &print, Model &model, double rib_width, const DynamicPrintConfig &config)
|
||||
{
|
||||
ModelObject *object = model.add_object();
|
||||
object->name = "slab_over_rib.stl";
|
||||
object->add_volume(make_cube(4., 5., 6.), ModelVolumeType::MODEL_PART, false);
|
||||
if (rib_width > 0.) {
|
||||
TriangleMesh rib = make_cube(20., rib_width, 5.);
|
||||
rib.translate(4.f, 0.f, 0.f);
|
||||
object->add_volume(std::move(rib), ModelVolumeType::MODEL_PART, false);
|
||||
}
|
||||
TriangleMesh slab = make_cube(20., 5., 1.);
|
||||
slab.translate(4.f, 0.f, 5.f);
|
||||
object->add_volume(std::move(slab), ModelVolumeType::MODEL_PART, false);
|
||||
object->add_instance();
|
||||
object->ensure_on_bed();
|
||||
|
||||
print.auto_assign_extruders(object);
|
||||
print.apply(model, config);
|
||||
print.validate();
|
||||
print.set_status_silent();
|
||||
return print;
|
||||
}
|
||||
|
||||
// Every setting the assertions below depend on, so none of them rests on a default. The wall line widths
|
||||
// are pinned because the rib widths above are chosen against the distance from the slab edge to its outer
|
||||
// wall, and min_feature_size because it is one of the two thresholds under test.
|
||||
DynamicPrintConfig printable_rib_config(const char *wall_generator, bool detect_thin_wall)
|
||||
{
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({
|
||||
{ "wall_generator", wall_generator },
|
||||
{ "layer_height", 0.2 }, // puts a layer boundary exactly on the top of the rib
|
||||
{ "initial_layer_print_height", 0.2 },
|
||||
{ "nozzle_diameter", "0.4" },
|
||||
{ "outer_wall_line_width", 0.42 },
|
||||
{ "inner_wall_line_width", 0.45 },
|
||||
{ "wall_loops", 2 },
|
||||
{ "detect_overhang_wall", true },
|
||||
{ "detect_thin_wall", detect_thin_wall },
|
||||
{ "min_feature_size", "25%" },
|
||||
{ "raft_layers", 0 },
|
||||
// Anything that adds, drops or reorders walls would move length between the roles being counted.
|
||||
{ "extra_perimeters_on_overhangs", false },
|
||||
{ "overhang_reverse", false },
|
||||
{ "only_one_wall_top", false },
|
||||
{ "only_one_wall_first_layer", false },
|
||||
{ "unsupported_wall_last", false },
|
||||
{ "sparse_infill_density", "15%" },
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
// Length of every overhang perimeter path on the layer at `print_z`, loops and open extrusions alike.
|
||||
double overhang_length_at(const Print &print, double print_z)
|
||||
{
|
||||
double len = 0.;
|
||||
const auto add_entity = [&len](const ExtrusionEntity *entity, auto &&self) -> void {
|
||||
const auto add_paths = [&len](const ExtrusionPaths &paths) {
|
||||
for (const ExtrusionPath &path : paths)
|
||||
if (path.role() == erOverhangPerimeter)
|
||||
len += path.length();
|
||||
};
|
||||
if (const auto *coll = dynamic_cast<const ExtrusionEntityCollection*>(entity)) {
|
||||
for (const ExtrusionEntity *child : coll->entities)
|
||||
self(child, self);
|
||||
} else if (const auto *loop = dynamic_cast<const ExtrusionLoop*>(entity)) {
|
||||
add_paths(loop->paths);
|
||||
} else if (const auto *multi = dynamic_cast<const ExtrusionMultiPath*>(entity)) {
|
||||
add_paths(multi->paths);
|
||||
} else if (const auto *path = dynamic_cast<const ExtrusionPath*>(entity)) {
|
||||
if (path->role() == erOverhangPerimeter)
|
||||
len += path->length();
|
||||
}
|
||||
};
|
||||
|
||||
for (const Layer *layer : print.objects().front()->layers()) {
|
||||
if (std::abs(layer->print_z - print_z) > EPSILON)
|
||||
continue;
|
||||
for (const LayerRegion *region : layer->regions())
|
||||
add_entity(®ion->perimeters, add_entity);
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// A sliver the wall generator prints nothing for holds nothing up, so it cannot be what decides that the
|
||||
// wall above it is not an overhang. The rib under the slab is the only thing that edge of the slab could
|
||||
// rest on: below the threshold of the active generator the slab has to come out exactly as it does with
|
||||
// no rib at all, and the last check is the control - a rib the generator does print anchors that wall,
|
||||
// without which the first check would hold for want of any sensitivity to the rib.
|
||||
TEST_CASE("A lower layer sliver too thin to print does not support the wall above it", "[Perimeters]")
|
||||
{
|
||||
const char *wall_generator = GENERATE("classic", "arachne");
|
||||
const bool detect_thin_wall = GENERATE(true, false);
|
||||
CAPTURE(wall_generator, detect_thin_wall);
|
||||
|
||||
auto overhang_for = [wall_generator, detect_thin_wall](double rib_width) {
|
||||
Print print;
|
||||
Model model;
|
||||
slab_over_rib(print, model, rib_width, printable_rib_config(wall_generator, detect_thin_wall));
|
||||
print.process();
|
||||
REQUIRE_FALSE(print.objects().empty());
|
||||
return overhang_length_at(print, slab_first_layer_z);
|
||||
};
|
||||
|
||||
const double no_rib = overhang_for(0.);
|
||||
const double unprintable = overhang_for(unprintable_rib);
|
||||
const double printable = overhang_for(printable_rib);
|
||||
|
||||
// Only where the slab meets the tower is it held up from below, so both of its 20mm walls overhang.
|
||||
REQUIRE(no_rib > scale_(30.));
|
||||
CHECK_THAT(unprintable, Catch::Matchers::WithinAbs(no_rib, scale_(1.)));
|
||||
// A rib that does get printed takes the 20mm outer wall running along it out of the overhangs.
|
||||
CHECK(printable < no_rib - scale_(15.));
|
||||
}
|
||||
|
||||
@@ -335,6 +335,10 @@ TEST_CASE("The shipped defaults size the tower from the flush matrix", "[WipeTow
|
||||
const double flush_volume = WipeTower2::estimate_semm_flush_volume(config, 2);
|
||||
const double expected = std::max(double(WipeTower::get_limit_depth_by_height(5.f)), flush_volume / (0.2 * 50.));
|
||||
CHECK_THAT(estimate(config, 2, 0.2, 5.).depth, WithinAbs(expected, 1e-6));
|
||||
|
||||
// The flush volume is nonzero for one slot, but a lone filament makes no tool change.
|
||||
REQUIRE(WipeTower2::estimate_semm_flush_volume(config, 1) > 0.);
|
||||
CHECK_THAT(estimate(config, 1, 0.2, 5.).depth, WithinAbs(0., 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("A config missing a tower key falls back to that key's default", "[WipeTowerEstimate]") {
|
||||
|
||||
Reference in New Issue
Block a user