Compare commits

..
Author SHA1 Message Date
Hanif Koh f6f7f7c77b Load a CLI Project's Printer and Process Settings as the GUI Does
The CLI filled only the keys a project lacked from its system preset.
The GUI builds a project preset differently: the project's values go
over the default preset, without the print-host keys, and every key
the project does not list in different_settings_to_system is refreshed
to its base system preset's current value. After a profile update the
two sliced the same project differently.

That step now lives in Preset::load_external_config, which takes plain
configs and gets the base preset from a callback, so the collection
lookup stays in PresetCollection. PresetBundle::project_different_keys
builds the kept-key set from a project's escaped entry, adding the
preset bookkeeping keys, and is used by both the GUI and the CLI.
PresetCollection::load_external_preset calls the shared step with no
change in behaviour.

The CLI now builds the project's printer and process configs with the
same step, passing the system preset from resolve_system_preset. That
drops the hand-kept skip list for print-host and variant-layout keys
and the legacy-key check: the shared step already excludes print-host
keys and maps per-variant values onto the base preset's variant layout.
The --uptodate path and a printer or process given on the command line
are left as they were.

Because the GUI's kept-key set always holds the bookkeeping keys, the
refresh runs for every project that names a system preset, so the CLI
now loads that preset's vendor on every such run.
2026-09-30 16:35:49 +08:00
Hanif Koh 526a30225f Fill Settings Missing From a CLI Project From Its System Presets
A project saved before a printer or process option existed has no value
for it. The GUI takes such keys from the project's system preset; the
CLI left them at the option default, so e.g. extruder_clearance_dist_to_rod
sliced as 40 instead of the P1S's 33.

The CLI now resolves the project's system printer and process presets by
name and copies the keys the project lacks, skipping preset bookkeeping,
print-host keys, the extruder variant layout and keys the legacy handler
drops. PresetBundle::resolve_system_preset finds the vendor through its
manifest or preset cache, so it also works in release builds, which ship
vendors as caches only.
2026-09-30 16:35:49 +08:00
5 changed files with 61 additions and 178 deletions
+3 -8
View File
@@ -1913,10 +1913,7 @@ int CLI::run(int argc, char **argv)
old_printable_width = static_cast<int>(old_printable_bbox.size().x()); old_printable_width = static_cast<int>(old_printable_bbox.size().x());
old_printable_depth = static_cast<int>(old_printable_bbox.size().y()); old_printable_depth = static_cast<int>(old_printable_bbox.size().y());
} }
// A 3mf can carry an empty project_settings.config - the models in old_printable_height = (int)(config.opt_float("printable_height"));
// 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")) if (config.option<ConfigOptionFloat>("extruder_clearance_height_to_rod"))
old_height_to_rod = config.opt_float("extruder_clearance_height_to_rod"); old_height_to_rod = config.opt_float("extruder_clearance_height_to_rod");
@@ -2595,8 +2592,7 @@ int CLI::run(int argc, char **argv)
orig_printable_width = static_cast<int>(orig_printable_bbox.size().x()); orig_printable_width = static_cast<int>(orig_printable_bbox.size().x());
orig_printable_depth = static_cast<int>(orig_printable_bbox.size().y()); orig_printable_depth = static_cast<int>(orig_printable_bbox.size().y());
} }
if (config.option<ConfigOptionFloat>("printable_height")) orig_printable_height = (int)(config.opt_float("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%") 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; %__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)) if ((orig_printable_width > 0) && (orig_printable_depth > 0) && (orig_printable_height > 0))
@@ -4662,8 +4658,7 @@ int CLI::run(int argc, char **argv)
BoundingBoxf temp_printable_bbox(temp_printable_area); BoundingBoxf temp_printable_bbox(temp_printable_area);
printer_plate.printable_width = static_cast<int>(temp_printable_bbox.size().x()); 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_depth = static_cast<int>(temp_printable_bbox.size().y());
if (config.option<ConfigOptionFloat>("printable_height")) printer_plate.printable_height = (int)(config.opt_float("printable_height"));
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
} }
if (temp_exclude_area.size() >= 4) { if (temp_exclude_area.size() >= 4) {
printer_plate.exclude_width = (int)(temp_exclude_area[2].x() - temp_exclude_area[0].x()); printer_plate.exclude_width = (int)(temp_exclude_area[2].x() - temp_exclude_area[0].x());
+58 -55
View File
@@ -5,7 +5,6 @@
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <iostream> #include <iostream>
#include <limits>
#include "FillBase.hpp" #include "FillBase.hpp"
#include "FillGyroid.hpp" #include "FillGyroid.hpp"
@@ -124,41 +123,44 @@ 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( static inline Polyline make_wave(
const std::vector<Vec2d>& one_period, double x_min, double x_max, double offset, double scaleFactor, bool vertical) 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 double period = one_period.back().x(); std::vector<Vec2d> points = one_period;
// The last sample of a period is the first one of the next. double period = points.back()(0);
const size_t n = one_period.size() - 1; if (width != period) // do not extend if already truncated
double x0 = std::floor(x_min / period) * period; {
size_t i = 0; points.reserve(one_period.size() * size_t(floor(width / period)));
while (i + 1 < n && x0 + one_period[i + 1].x() <= x_min) points.pop_back();
++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 polyline;
polyline.points.reserve(size_t((x_max - x0) / period + 1.) * n + 1); polyline.points.reserve(points.size());
for (;;) { for (auto& point : points) {
Vec2d point(x0 + one_period[i].x(), one_period[i].y() + offset); point(1) += offset;
const bool last = point.x() >= x_max; point(1) = std::clamp(double(point.y()), 0., height);
if (vertical) if (vertical)
std::swap(point(0), point(1)); std::swap(point(0), point(1));
polyline.points.emplace_back((point * scaleFactor).cast<coord_t>()); polyline.points.emplace_back((point * scaleFactor).cast<coord_t>());
if (last)
break;
if (++i == n) {
i = 0;
x0 += period;
}
} }
return polyline; return polyline;
} }
static std::vector<Vec2d> make_one_period(double scaleFactor, double z_cos, double z_sin, bool vertical, bool flip, double tolerance) static std::vector<Vec2d> make_one_period(double width, double scaleFactor, double z_cos, double z_sin, bool vertical, bool flip, double tolerance)
{ {
std::vector<Vec2d> points; std::vector<Vec2d> points;
double dx = M_PI_2; // exact coordinates on main inflexion lobes double dx = M_PI_2; // exact coordinates on main inflexion lobes
double limit = 2*M_PI; double limit = std::min(2*M_PI, width);
points.reserve(coord_t(ceil(limit / tolerance / 3))); points.reserve(coord_t(ceil(limit / tolerance / 3)));
for (double x = 0.; x < limit - EPSILON; x += dx) { for (double x = 0.; x < limit - EPSILON; x += dx) {
@@ -237,8 +239,7 @@ static inline double compute_omega_factor(double density_adjusted, double line_s
return std::clamp(raw, 1.0, 2.0); return std::clamp(raw, 1.0, 2.0);
} }
// Waves covering bbox, with the pattern anchored at origin. static Polylines make_gyroid_waves(double gridZ, double density_adjusted, double line_spacing, double width, double height)
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; const double scaleFactor = scale_(line_spacing) / density_adjusted;
@@ -253,36 +254,29 @@ static Polylines make_gyroid_waves(double gridZ, double density_adjusted, double
const double z_cos = cos(z); const double z_cos = cos(z);
bool vertical = (std::abs(z_sin) <= std::abs(z_cos)); 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 lower_bound = 0.;
double upper_bound = height;
bool flip = true; bool flip = true;
if (vertical) { if (vertical) {
flip = false; flip = false;
lower_bound = -M_PI; lower_bound = -M_PI;
std::swap(lo(0), lo(1)); upper_bound = width - M_PI_2;
std::swap(hi(0), hi(1)); std::swap(width,height);
} }
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 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
flip = !flip; // even polylines are a bit shifted flip = !flip; // even polylines are a bit shifted
std::vector<Vec2d> one_period_even = make_one_period(scaleFactor, z_cos, z_sin, vertical, flip, tolerance); std::vector<Vec2d> one_period_even = make_one_period(width, scaleFactor, z_cos, z_sin, vertical, flip, tolerance);
// 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; 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(), for (double y0 = lower_bound; y0 < upper_bound + EPSILON; y0 += M_PI) {
lower_bound + i * M_PI, scaleFactor, vertical)); // creates odd polylines
wave.translate(origin); 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));
}
} }
return result; return result;
@@ -308,12 +302,12 @@ void FillGyroid::_fill_surface_single(
// Distance between the gyroid waves in scaled coordinates. // Distance between the gyroid waves in scaled coordinates.
coord_t distance = coord_t(scale_(this->spacing) / density_adjusted); coord_t distance = coord_t(scale_(this->spacing) / density_adjusted);
// Anchor the pattern to our grid module; the 10-line shift keeps its established phase. // align bounding box to a multiple of our grid module
const coord_t shift = coord_t(10 * scale_(this->spacing)); bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)));
const Point origin = align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)) - Point(shift, shift);
// Keep the pattern ends and the multiline copies outside the contour. // Expand the bounding box to avoid artifacts at the edges
bb.offset(scale_(this->spacing * params.multiline)); coord_t expand = 10 * (scale_(this->spacing));
bb.offset(expand);
// generate pattern // generate pattern
Polylines polylines; Polylines polylines;
@@ -333,14 +327,23 @@ void FillGyroid::_fill_surface_single(
const float density_factor = std::max(0.001f, float(params.density * DensityAdjust / params.multiline)); 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; const float period = float(2.0 * M_PI) * float(this->spacing) / density_factor;
// A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region. // bb is already expanded above by 10 * scale_(spacing) for edge artifacts;
const coord_t cell = scaled(marchsq::GyroidField::gsizef); // skip a second offset here to avoid raster-area bloat in the marching squares pass.
bb.offset(cell);
bb.merge(align_to_grid(bb.min, Point(cell, cell)));
marchsq::GyroidField sf(bb, this->z, period, float(omega)); marchsq::GyroidField sf(bb, this->z, period, float(omega));
polylines = marchsq::get_gyroid_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION); polylines = marchsq::get_gyroid_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION);
} else { } else {
polylines = make_gyroid_waves(scale_(this->z), density_adjusted, this->spacing, bb, origin); 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);
} }
// Apply multiline offset if needed // Apply multiline offset if needed
-7
View File
@@ -11,13 +11,6 @@ endif ()
add_test(NAME cli_strict_mode add_test(NAME cli_strict_mode
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_strict.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON}) 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 set_tests_properties(cli_strict_mode PROPERTIES
LABELS "CLI;RequiresApp" LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77 SKIP_RETURN_CODE 77
@@ -1,52 +0,0 @@
#!/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"
-56
View File
@@ -13,7 +13,6 @@
#include "libslic3r/AABBTreeLines.hpp" #include "libslic3r/AABBTreeLines.hpp"
#include "libslic3r/Fill/Fill.hpp" #include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Fill/FillAdaptive.hpp" #include "libslic3r/Fill/FillAdaptive.hpp"
#include "libslic3r/Fill/FillGyroid.hpp"
#include "libslic3r/Flow.hpp" #include "libslic3r/Flow.hpp"
#include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry.hpp"
#include "libslic3r/IntersectionPoints.hpp" #include "libslic3r/IntersectionPoints.hpp"
@@ -1258,61 +1257,6 @@ 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]") TEST_CASE("Multiline cubic infill follows the cubic lines without crossing itself", "[Fill]")
{ {
const int multiline = GENERATE(2, 3); const int multiline = GENERATE(2, 3);