Compare commits

..
Author SHA1 Message Date
Rodrigo Faselli 4709ba241c Merge branch 'main' into fix/cli-printable-height 2026-09-30 10:53:38 -03:00
Ian BassiandRodrigo Faselli 97700c5ab5 Gyroid Optimization (#16002)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-09-30 10:21:24 -03:00
Kris Austin ff2f42016a Fix CLI crash on a 3mf without project settings (#16004)
When a 3mf is tagged as written by OrcaSlicer or BambuStudio, the CLI
treats it as a project and reads the printer settings stored in the
file. #14580 guarded four of those reads, but printable_height still
called opt_float() without a check, so a 3mf whose
project_settings.config is empty or missing crashed with a null
dereference. The handy models OrcaBadge.3mf and OrcaSliced.3mf are both
like this.

Skip the read when the option is absent, like the reads around it. The
bed-size logic further down already treats an old printable height of 0
as unknown and uses the current printer's.
2026-09-30 10:04:14 -03:00
ExPikaPaka 771ed20d97 Add a CLI regression test for a project with empty settings
Runs --info over a copy of a shipped model whose Metadata/project_settings.config
has been rewritten to an empty object, so the test keeps covering the crash no
matter what settings the shipped models carry later.

Verified both ways: the test passes against this branch and fails with a
segmentation fault against a build without the guards.
2026-09-30 09:21:39 +02:00
ExPikaPaka 616f1e4336 Fix a crash on loading a 3MF with empty project settings
opt_float() dereferences what option<>() returns without checking it, and
option<>() is called with create = false. Three CLI sites read printable_height
that way, so a 3mf whose Metadata/project_settings.config holds an empty object
takes the CLI down with a null dereference. Both models shipped in
resources/handy_models are such files, so `--info` on either of them segfaults.

Guard the three reads the way the neighbouring reads of
extruder_clearance_height_to_rod and friends already are. All three target
variables are initialised to 0 and the consumer tests for > 0, so an absent
setting already had a defined meaning and nothing changes for a project that
carries the setting.
2026-09-30 09:17:43 +02:00
8 changed files with 192 additions and 130 deletions
+12 -40
View File
@@ -1913,7 +1913,10 @@ int CLI::run(int argc, char **argv)
old_printable_width = static_cast<int>(old_printable_bbox.size().x());
old_printable_depth = static_cast<int>(old_printable_bbox.size().y());
}
old_printable_height = (int)(config.opt_float("printable_height"));
// A 3mf can carry an empty project_settings.config - the models in
// resources/handy_models do - and opt_float() dereferences without checking.
if (config.option<ConfigOptionFloat>("printable_height"))
old_printable_height = (int)(config.opt_float("printable_height"));
if (config.option<ConfigOptionFloat>("extruder_clearance_height_to_rod"))
old_height_to_rod = config.opt_float("extruder_clearance_height_to_rod");
@@ -2592,7 +2595,8 @@ int CLI::run(int argc, char **argv)
orig_printable_width = static_cast<int>(orig_printable_bbox.size().x());
orig_printable_depth = static_cast<int>(orig_printable_bbox.size().y());
}
orig_printable_height = (int)(config.opt_float("printable_height"));
if (config.option<ConfigOptionFloat>("printable_height"))
orig_printable_height = (int)(config.opt_float("printable_height"));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%")
%__LINE__ %old_printable_width %orig_printable_width %old_printable_depth %orig_printable_depth %old_printable_height %orig_printable_height;
if ((orig_printable_width > 0) && (orig_printable_depth > 0) && (orig_printable_height > 0))
@@ -2779,37 +2783,6 @@ int CLI::run(int argc, char **argv)
fetch_compatible_values = true;
}
// Refresh every project filament no loaded filament replaces from its current system preset, as the GUI
// does when it loads the project; the filament merge below keeps the keys the project lists as changed.
// Entries stay in slot order, which the merge's variant bookkeeping relies on.
std::vector<bool> load_filaments_refresh(load_filaments_config.size(), false);
if (is_bbl_3mf && new_printer_name.empty()) {
const ConfigOptionStrings *project_filament_ids = m_print_config.option<ConfigOptionStrings>("filament_ids");
for (size_t index = 0; index < current_filaments_system_name.size(); index++) {
const int slot = static_cast<int>(index) + 1;
if (std::find(load_filaments_index.begin(), load_filaments_index.end(), slot) != load_filaments_index.end())
continue;
std::string system_name = current_filaments_system_name[index];
if (system_name.empty())
continue;
PresetBundle::convert_filament_preset_name(current_printer_name, system_name);
DynamicPrintConfig config;
std::string error;
if (!ensure_system_preset_resolver().resolve_system_preset(config, Preset::TYPE_FILAMENT, system_name, config_substitution_rule, error)) {
BOOST_LOG_TRIVIAL(warning) << boost::format("CLI: system filament preset '%1%' not resolved (%2%); filament %3% keeps its values") % system_name % error % slot;
continue;
}
const size_t at = std::upper_bound(load_filaments_index.begin(), load_filaments_index.end(), slot) - load_filaments_index.begin();
load_filaments_id.insert(load_filaments_id.begin() + at,
project_filament_ids != nullptr && index < project_filament_ids->size() ? project_filament_ids->values[index] : std::string());
load_filaments_name.insert(load_filaments_name.begin() + at, system_name);
load_filaments_config.insert(load_filaments_config.begin() + at, std::move(config));
load_filaments_index.insert(load_filaments_index.begin() + at, slot);
load_filaments_inherit.insert(load_filaments_inherit.begin() + at, system_name);
load_filaments_refresh.insert(load_filaments_refresh.begin() + at, true);
}
}
//fetch upward_compatible_machine
if (fetch_upward_values) {
if (!current_printer_system_name.empty()) {
@@ -3500,7 +3473,7 @@ int CLI::run(int argc, char **argv)
}
//set the filament settings into print config
if ((load_filament_count > 0) || (up_config_to_date) || !load_filaments_config.empty())
if ((load_filament_count > 0) || (up_config_to_date))
{
//std::vector<int> filament_variant_count(filament_count, 1);
std::vector<int> old_start_indice(filament_count, 0);
@@ -3559,8 +3532,6 @@ int CLI::run(int argc, char **argv)
for (int index = 0; index < load_filaments_config.size(); index++) {
DynamicPrintConfig& config = load_filaments_config[index];
int filament_index = load_filaments_index[index];
// A filament given with --load-filaments replaces the slot; a refreshed one keeps the project's changed keys.
const bool loaded = load_filament_count > 0 && !load_filaments_refresh[index];
std::vector<std::string> different_keys;
//ORCA: diff before load_default_gcodes_to_config, the way the process and machine
@@ -3570,12 +3541,12 @@ int CLI::run(int argc, char **argv)
// compared" to "compared as empty against the parent" and land in the column
// as an override the user never made.
std::string filament_different_settings;
if (loaded)
if (load_filament_count > 0)
filament_different_settings = cli_different_settings(config, load_filaments_inherit[index], Preset::TYPE_FILAMENT);
load_default_gcodes_to_config(config, Preset::TYPE_FILAMENT);
if (loaded) {
if (load_filament_count > 0) {
ConfigOptionStrings *opt_filament_settings = static_cast<ConfigOptionStrings *> (m_print_config.option("filament_settings_id", true));
std::string& filament_name = load_filaments_name[index];
ConfigOptionString* filament_name_setting = new ConfigOptionString(filament_name);
@@ -3647,7 +3618,7 @@ int CLI::run(int argc, char **argv)
flush_and_exit(CLI_CONFIG_FILE_ERROR);
}
if (!loaded && !different_keys_set.empty())
if ((load_filament_count == 0) && !different_keys_set.empty())
{
std::set<std::string>::iterator iter = different_keys_set.find(opt_key);
if ( iter != different_keys_set.end()) {
@@ -4691,7 +4662,8 @@ int CLI::run(int argc, char **argv)
BoundingBoxf temp_printable_bbox(temp_printable_area);
printer_plate.printable_width = static_cast<int>(temp_printable_bbox.size().x());
printer_plate.printable_depth = static_cast<int>(temp_printable_bbox.size().y());
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
if (config.option<ConfigOptionFloat>("printable_height"))
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
}
if (temp_exclude_area.size() >= 4) {
printer_plate.exclude_width = (int)(temp_exclude_area[2].x() - temp_exclude_area[0].x());
+54 -57
View File
@@ -5,6 +5,7 @@
#include <cmath>
#include <algorithm>
#include <iostream>
#include <limits>
#include "FillBase.hpp"
#include "FillGyroid.hpp"
@@ -123,44 +124,41 @@ static inline double f(double x, double z_sin, double z_cos, bool vertical, bool
}
}
// Repeats one period of a wave from the last sample at or before x_min to the first one at or after x_max.
static inline Polyline make_wave(
const std::vector<Vec2d>& one_period, double width, double height, double offset, double scaleFactor,
double z_cos, double z_sin, bool vertical, bool flip)
const std::vector<Vec2d>& one_period, double x_min, double x_max, double offset, double scaleFactor, bool vertical)
{
std::vector<Vec2d> points = one_period;
double period = points.back()(0);
if (width != period) // do not extend if already truncated
{
points.reserve(one_period.size() * size_t(floor(width / period)));
points.pop_back();
const double period = one_period.back().x();
// The last sample of a period is the first one of the next.
const size_t n = one_period.size() - 1;
double x0 = std::floor(x_min / period) * period;
size_t i = 0;
while (i + 1 < n && x0 + one_period[i + 1].x() <= x_min)
++i;
size_t n = points.size();
do {
points.emplace_back(points[points.size()-n].x() + period, points[points.size()-n].y());
} while (points.back()(0) < width - EPSILON);
points.emplace_back(Vec2d(width, f(width, z_sin, z_cos, vertical, flip)));
}
// and construct the final polyline to return:
Polyline polyline;
polyline.points.reserve(points.size());
for (auto& point : points) {
point(1) += offset;
point(1) = std::clamp(double(point.y()), 0., height);
polyline.points.reserve(size_t((x_max - x0) / period + 1.) * n + 1);
for (;;) {
Vec2d point(x0 + one_period[i].x(), one_period[i].y() + offset);
const bool last = point.x() >= x_max;
if (vertical)
std::swap(point(0), point(1));
polyline.points.emplace_back((point * scaleFactor).cast<coord_t>());
if (last)
break;
if (++i == n) {
i = 0;
x0 += period;
}
}
return polyline;
}
static std::vector<Vec2d> make_one_period(double width, double scaleFactor, double z_cos, double z_sin, bool vertical, bool flip, double tolerance)
static std::vector<Vec2d> make_one_period(double scaleFactor, double z_cos, double z_sin, bool vertical, bool flip, double tolerance)
{
std::vector<Vec2d> points;
double dx = M_PI_2; // exact coordinates on main inflexion lobes
double limit = std::min(2*M_PI, width);
double limit = 2*M_PI;
points.reserve(coord_t(ceil(limit / tolerance / 3)));
for (double x = 0.; x < limit - EPSILON; x += dx) {
@@ -239,7 +237,8 @@ static inline double compute_omega_factor(double density_adjusted, double line_s
return std::clamp(raw, 1.0, 2.0);
}
static Polylines make_gyroid_waves(double gridZ, double density_adjusted, double line_spacing, double width, double height)
// Waves covering bbox, with the pattern anchored at origin.
static Polylines make_gyroid_waves(double gridZ, double density_adjusted, double line_spacing, const BoundingBox &bbox, const Point &origin)
{
const double scaleFactor = scale_(line_spacing) / density_adjusted;
@@ -254,29 +253,36 @@ static Polylines make_gyroid_waves(double gridZ, double density_adjusted, double
const double z_cos = cos(z);
bool vertical = (std::abs(z_sin) <= std::abs(z_cos));
// Range to cover in pattern units, with the waves running along x.
Vec2d lo = (bbox.min - origin).cast<double>() / scaleFactor;
Vec2d hi = (bbox.max - origin).cast<double>() / scaleFactor;
double lower_bound = 0.;
double upper_bound = height;
bool flip = true;
if (vertical) {
flip = false;
lower_bound = -M_PI;
upper_bound = width - M_PI_2;
std::swap(width,height);
std::swap(lo(0), lo(1));
std::swap(hi(0), hi(1));
}
std::vector<Vec2d> one_period_odd = make_one_period(width, scaleFactor, z_cos, z_sin, vertical, flip, tolerance); // creates one period of the waves, so it doesn't have to be recalculated all the time
std::vector<Vec2d> one_period_odd = make_one_period(scaleFactor, z_cos, z_sin, vertical, flip, tolerance); // creates one period of the waves, so it doesn't have to be recalculated all the time
flip = !flip; // even polylines are a bit shifted
std::vector<Vec2d> one_period_even = make_one_period(width, scaleFactor, z_cos, z_sin, vertical, flip, tolerance);
Polylines result;
std::vector<Vec2d> one_period_even = make_one_period(scaleFactor, z_cos, z_sin, vertical, flip, tolerance);
for (double y0 = lower_bound; y0 < upper_bound + EPSILON; y0 += M_PI) {
// creates odd polylines
result.emplace_back(make_wave(one_period_odd, width, height, y0, scaleFactor, z_cos, z_sin, vertical, flip));
// creates even polylines
y0 += M_PI;
if (y0 < upper_bound + EPSILON) {
result.emplace_back(make_wave(one_period_even, width, height, y0, scaleFactor, z_cos, z_sin, vertical, flip));
// Every wave spans [offset + f_min, offset + f_max] across.
double f_min = std::numeric_limits<double>::max();
double f_max = std::numeric_limits<double>::lowest();
for (const std::vector<Vec2d> *one_period : { &one_period_odd, &one_period_even })
for (const Vec2d &point : *one_period) {
f_min = std::min(f_min, point.y());
f_max = std::max(f_max, point.y());
}
Polylines result;
for (int i = int(std::ceil((lo.y() - f_max - lower_bound) / M_PI)); lower_bound + i * M_PI + f_min <= hi.y(); ++i) {
Polyline &wave = result.emplace_back(make_wave(i % 2 == 0 ? one_period_odd : one_period_even, lo.x(), hi.x(),
lower_bound + i * M_PI, scaleFactor, vertical));
wave.translate(origin);
}
return result;
@@ -302,12 +308,12 @@ void FillGyroid::_fill_surface_single(
// Distance between the gyroid waves in scaled coordinates.
coord_t distance = coord_t(scale_(this->spacing) / density_adjusted);
// align bounding box to a multiple of our grid module
bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)));
// Anchor the pattern to our grid module; the 10-line shift keeps its established phase.
const coord_t shift = coord_t(10 * scale_(this->spacing));
const Point origin = align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)) - Point(shift, shift);
// Expand the bounding box to avoid artifacts at the edges
coord_t expand = 10 * (scale_(this->spacing));
bb.offset(expand);
// Keep the pattern ends and the multiline copies outside the contour.
bb.offset(scale_(this->spacing * params.multiline));
// generate pattern
Polylines polylines;
@@ -327,23 +333,14 @@ void FillGyroid::_fill_surface_single(
const float density_factor = std::max(0.001f, float(params.density * DensityAdjust / params.multiline));
const float period = float(2.0 * M_PI) * float(this->spacing) / density_factor;
// bb is already expanded above by 10 * scale_(spacing) for edge artifacts;
// skip a second offset here to avoid raster-area bloat in the marching squares pass.
// A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region.
const coord_t cell = scaled(marchsq::GyroidField::gsizef);
bb.offset(cell);
bb.merge(align_to_grid(bb.min, Point(cell, cell)));
marchsq::GyroidField sf(bb, this->z, period, float(omega));
polylines = marchsq::get_gyroid_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION);
} else {
polylines = make_gyroid_waves(
scale_(this->z),
density_adjusted,
this->spacing,
ceil(bb.size()(0) / distance) + 1.,
ceil(bb.size()(1) / distance) + 1.);
// The parametric generator produces wave coords relative to the grid origin;
// shift them into absolute layer coords. The marching-squares branch above
// already emits absolute coords via GyroidField::to_Point, so it skips this.
for (Polyline &pl : polylines)
pl.translate(bb.min);
polylines = make_gyroid_waves(scale_(this->z), density_adjusted, this->spacing, bb, origin);
}
// Apply multiline offset if needed
+1 -1
View File
@@ -5251,7 +5251,7 @@ static void apply_receiver_mix_relocations(DynamicPrintConfig&
//convert the old filament preset to new one after split
void PresetBundle::convert_filament_preset_name(const std::string& machine_name, std::string& filament_name)
static void convert_filament_preset_name(std::string& machine_name, std::string& filament_name)
{
auto machine_iter = filament_preset_convert.find(machine_name);
if (machine_iter != filament_preset_convert.end())
-3
View File
@@ -238,9 +238,6 @@ public:
// Keys a project keeps when its presets are loaded: those listed in its escaped
// "different_settings_to_system" entry for the preset, plus the preset bookkeeping keys.
static std::set<std::string> project_different_keys(const std::string &different_settings);
// A project filament saved under a name the current presets split per nozzle (e.g. H2D 0.6) is loaded from
// the preset that now holds its values.
static void convert_filament_preset_name(const std::string& machine_name, std::string& filament_name);
PresetBundle();
PresetBundle(const PresetBundle &rhs);
+7
View File
@@ -11,6 +11,13 @@ endif ()
add_test(NAME cli_strict_mode
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_strict.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON})
add_test(NAME cli_empty_project_config
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_empty_project_config.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON} ${CMAKE_SOURCE_DIR})
set_tests_properties(cli_empty_project_config PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
TIMEOUT 300)
set_tests_properties(cli_strict_mode PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Regression check: a 3mf whose Metadata/project_settings.config carries no settings must load.
#
# The CLI reads printable_height out of the project config with opt_float(), which dereferences
# what option<>() returns. With create = false that is null when the key is absent, so a project
# saved without settings used to take the CLI down with a segfault. Both models in
# resources/handy_models are such files.
#
# usage: test_cli_empty_project_config.sh <orca-slicer binary> <python3> <source dir>
set -u
BIN="${1:-}"
PY="${2:-python3}"
SRC="${3:-}"
# 77 is the test's SKIP_RETURN_CODE.
[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; }
[ -f "$SRC/resources/handy_models/OrcaBadge.3mf" ] || { echo "SKIP: handy model not found"; exit 77; }
WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-emptycfg.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/datadir"
# Rewrite the project settings to an empty object, so the test holds no matter what the shipped
# models carry later on.
cp "$SRC/resources/handy_models/OrcaBadge.3mf" "$WORK/empty_config.3mf"
"$PY" - "$WORK/empty_config.3mf" <<'PYEOF'
import shutil, sys, zipfile
path = sys.argv[1]
entry = "Metadata/project_settings.config"
with zipfile.ZipFile(path) as src:
items = [(i, src.read(i.filename)) for i in src.infolist()]
with zipfile.ZipFile(path + ".new", "w", zipfile.ZIP_DEFLATED) as dst:
seen = False
for info, data in items:
if info.filename == entry:
data, seen = b"{\n}\n", True
dst.writestr(info, data)
if not seen:
dst.writestr(entry, b"{\n}\n")
shutil.move(path + ".new", path)
PYEOF
"$BIN" --datadir "$WORK/datadir" --info "$WORK/empty_config.3mf" > "$WORK/info.txt" 2>&1
rc=$?
if [ $rc -ne 0 ]; then
echo "FAIL: --info on a project with empty settings exited $rc"
tail -20 "$WORK/info.txt"
exit 1
fi
grep -q "size_x" "$WORK/info.txt" || { echo "FAIL: --info printed no geometry"; cat "$WORK/info.txt"; exit 1; }
echo "PASS: a project with empty settings loads"
+10 -29
View File
@@ -1,13 +1,11 @@
#!/usr/bin/env bash
# End-to-end check that the CLI loads a project's printer, process and filament settings as the GUI does.
# End-to-end check that the CLI loads a project's printer and process settings as the GUI does.
#
# The GUI takes every key a project does not list as changed from the project's current system preset:
# keys saved before an option existed, and keys holding an older system value. Keys the project lists
# in different_settings_to_system keep the project's value. A project is exported from the shipped
# Bambu Lab P1S presets with two filaments; one printer key and one process key are removed, one printer
# key, one process key and two filament keys (one per filament, one per extruder variant) are changed
# without being listed, one process key and the first filament's density are changed and listed, and it
# is sliced again: as is, with --uptodate, and with --load-filaments replacing only the second filament.
# Bambu Lab P1S presets; one printer key and one process key are removed, one printer key and one
# process key are changed without being listed, one key is changed and listed, and it is sliced again.
#
# usage: test_cli_project_missing_keys.sh <orca-slicer binary> <python3> <resources/profiles/BBL>
set -u
@@ -47,7 +45,7 @@ slice() {
slice base "$WORK/cube.stl" \
--load-settings "$PROFILES/machine/Bambu Lab P1S 0.4 nozzle.json;$PROFILES/process/0.20mm Standard @BBL X1C.json" \
--load-filaments "$PROFILES/filament/Bambu PLA Basic @BBL P1S 0.4 nozzle.json;$PROFILES/filament/Bambu PLA Basic @BBL P1S 0.4 nozzle.json"
--load-filaments "$PROFILES/filament/Bambu PLA Basic @BBL P1S 0.4 nozzle.json"
# The removed keys, with their option defaults from PrintConfig.cpp; stale keys changed without being
# listed as different, which must come back with the system value; and a listed key the project keeps.
@@ -69,47 +67,30 @@ with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED
for key in ("top_shell_layers", "extruder_clearance_height_to_rod"):
expected[key] = config[key]
config[key] = str(int(float(config[key])) + 1)
for key in ("filament_cost", "filament_max_volumetric_speed"):
expected[key] = config[key]
config[key] = [str(float(v) + 1) for v in config[key]]
expected["wall_loops"] = str(int(config["wall_loops"]) + 1)
config["wall_loops"] = expected["wall_loops"]
expected["filament_density"] = [str(float(config["filament_density"][0]) + 1)] + config["filament_density"][1:]
config["filament_density"] = [expected["filament_density"][0]] + [str(float(v) + 1) for v in config["filament_density"][1:]]
# One entry for the process, one per filament, one for the printer.
different = config["different_settings_to_system"]
different[0] = ";".join([k for k in different[0].split(";") if k] + ["wall_loops"])
different[1] = ";".join([k for k in different[1].split(";") if k] + ["filament_density"])
data = json.dumps(config, indent=4)
zout.writestr(item, data)
with open(dst + ".expected.json", "w") as f:
json.dump(expected, f)
EOF
check() {
if ! "$PY" - "$WORK/$1/out.3mf" "$WORK/old.3mf.expected.json" "$1" <<'EOF'
slice project "$WORK/old.3mf"
"$PY" - "$WORK/project/out.3mf" "$WORK/old.3mf.expected.json" <<'EOF'
import json, sys, zipfile
with zipfile.ZipFile(sys.argv[1]) as z:
config = json.loads(z.read("Metadata/project_settings.config"))
with open(sys.argv[2]) as f:
expected = json.load(f)
errors = ["%s: %s is %r, want %r" % (sys.argv[3], key, config.get(key), want) for key, want in expected.items() if config.get(key) != want]
errors = ["%s is %r, want %r" % (key, config.get(key), want) for key, want in expected.items() if config.get(key) != want]
for e in errors:
print("FAIL: " + e)
sys.exit(1 if errors else 0)
EOF
then
tail -n 40 "$WORK/$1/log"
exit 1
fi
}
slice project "$WORK/old.3mf"
check project
slice uptodate "$WORK/old.3mf" --uptodate
check uptodate
# The replaced second filament takes the system values the refresh would have given it.
slice partial "$WORK/old.3mf" --load-filaments ";$PROFILES/filament/Bambu PLA Basic @BBL P1S 0.4 nozzle.json"
check partial
status=$?
[ "$status" -eq 0 ] || { tail -n 40 "$WORK/project/log"; exit 1; }
echo "PASS"
+56
View File
@@ -13,6 +13,7 @@
#include "libslic3r/AABBTreeLines.hpp"
#include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Fill/FillAdaptive.hpp"
#include "libslic3r/Fill/FillGyroid.hpp"
#include "libslic3r/Flow.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/IntersectionPoints.hpp"
@@ -1257,6 +1258,61 @@ TEST_CASE("Multiline infill of an object matches the infill of a larger object w
}
}
TEST_CASE("Gyroid infill of an object matches the infill of a larger object with the same center", "[Fill]")
{
const bool optimized = GENERATE(false, true);
const int multiline = GENERATE(1, 2);
const float density = GENERATE(0.05f, 0.2f);
const double spacing = 0.45;
CAPTURE(optimized, multiline, density);
auto circle = [](double radius) {
Polygon contour = make_circle_num_segments(scale_(radius), 120);
contour.translate(Point::new_scale(100., 60.));
return ExPolygon(std::move(contour));
};
const ExPolygon object = circle(20.);
const ExPolygon larger = circle(30.);
auto fill = [optimized, multiline, density, spacing](const ExPolygon &region, double z) {
std::unique_ptr<Fill> filler(Fill::new_from_type(ipGyroid));
filler->spacing = spacing;
filler->angle = float(M_PI / 7.);
filler->z = z;
FillParams params;
params.density = density;
params.multiline = multiline;
params.gyroid_optimized = optimized;
params.dont_adjust = true;
Surface surface(stInternal, region);
return filler->fill_surface(&surface, params);
};
// Away from the boundary of the object, where both are clipped and connected the same way.
const Polygons inner = shrink(to_polygons(object), scale_(1.));
auto farthest = [&inner](const Polylines &from, const Polylines &to) {
const AABBTreeLines::LinesDistancer<Line> tree(to_lines(to));
double distance = 0.;
for (const Polyline &path : intersection_pl(from, inner))
for (const Point &point : path.equally_spaced_points(scale_(0.2)))
distance = std::max(distance, tree.distance_from_lines<false>(point));
return unscale<double>(distance);
};
// Marching squares simplifies rings that start elsewhere in each object.
const double tolerance = optimized ? SPARSE_INFILL_RESOLUTION + 0.01 : 0.01;
// Half a z period of the waves, through both switches between horizontal and vertical waves.
const double wave_distance = spacing * multiline / (density * FillGyroid::DensityAdjust);
for (int step = 0; step <= 8; ++step) {
const double z = wave_distance * M_PI * step / 8.;
CAPTURE(z);
const Polylines paths = fill(object, z);
REQUIRE_FALSE(paths.empty());
const Polylines reference = fill(larger, z);
CHECK(farthest(reference, paths) < tolerance);
CHECK(farthest(paths, reference) < tolerance);
}
}
TEST_CASE("Multiline cubic infill follows the cubic lines without crossing itself", "[Fill]")
{
const int multiline = GENERATE(2, 3);