Compare commits

..
12 changed files with 197 additions and 32 deletions
+4 -6
View File
@@ -330,12 +330,13 @@ 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.
- name: Inject commit hash and flatpak-builder cache buster into Flatpak manifest
# 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
env:
flatpak_builder_cache_buster: ${{ github.run_id }}-${{ github.run_attempt }}
run: |
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\"|}" \
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\"|}" \
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
shell: bash
# flatpak-builder's --ccache only wraps cc and gcc, and the manifest builds
@@ -381,9 +382,6 @@ 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.
+1 -1
View File
@@ -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 "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."
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."
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 via the action's run-tests, then shipped to a separate
# Built (not run) here when CI injects 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
+3 -1
View File
@@ -776,7 +776,9 @@ 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);
assert(raw_opt != nullptr);
// 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");
if (raw_opt->type() != coFloatOrPercent)
throw ConfigurationError("ConfigBase::get_abs_value(): opt_key is not of coFloatOrPercent");
// Compute absolute value.
+9 -10
View File
@@ -107,12 +107,6 @@ 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;
@@ -157,6 +151,14 @@ 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
@@ -169,10 +171,7 @@ 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 {
// 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;
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;
+7 -3
View File
@@ -120,9 +120,6 @@ 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;
@@ -135,6 +132,13 @@ 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
+15
View File
@@ -2885,6 +2885,21 @@ 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);
+2
View File
@@ -149,6 +149,8 @@ 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);
+14 -3
View File
@@ -11,12 +11,23 @@ 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;
double filament_max_volumetric_speed = config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->get_at(filament_idx);
// 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);
// todo multi_extruders:
const float nozzle_diameter = config.option<ConfigOptionFloats>("nozzle_diameter")->get_at(extruder_id);
const float nozzle_diameter = vector_at("nozzle_diameter", 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, config.option<ConfigOptionFloatsNullable>("outer_wall_speed")->get_at(extruder_id)),
auto pa_speed = std::min(std::max(general_suggested_min_speed, nullable_at("outer_wall_speed", extruder_id)),
filament_max_volumetric_speed / pattern_line.mm3_per_mm());
return std::floor(pa_speed);
+6 -3
View File
@@ -234,8 +234,10 @@ 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.
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)));
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)));
}
#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++));
@@ -1344,7 +1346,8 @@ GLGizmoBase* GLGizmosManager::get_current() const
GLGizmoBase* GLGizmosManager::get_gizmo(GLGizmosManager::EType type) const
{
return ((type == Undefined) || m_gizmos.empty()) ? nullptr : m_gizmos[type].get();
// m_gizmos ends before the enum does when the CAD gizmos are not registered.
return type < m_gizmos.size() ? m_gizmos[type].get() : nullptr;
}
GLGizmosManager::EType GLGizmosManager::get_gizmo_from_name(const std::string& gizmo_name) const
+135
View File
@@ -495,3 +495,138 @@ 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(&region->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,10 +335,6 @@ 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]") {