Compare commits

...

3 Commits

Author SHA1 Message Date
Hanif Koh
df490868d1 Name the missing parent when a dropped preset is resolved from the CLI
When load_presets() drops a preset because its parent does not exist, the
only trace is a log line. The CLI then resolves --load-settings /
--load-filaments against the loaded bundle and reports "Preset was not found
in the loaded bundle", which points at the resolver rather than at the real
cause.

Record the presets dropped for a missing parent in the collection, keyed by
file, and have resolve_preset_config() report that parent by name when the
source file is one of them.
2026-09-09 18:02:41 +08:00
Hanif Koh
6cc84bc10e Retry unresolved parents when loading presets from a directory
A preset that inherits another preset from the same directory was dropped at
load time. load_presets() resolves "inherits" with find_preset2(), which
searches m_presets, but the presets it loads are staged in a local vector and
merged into m_presets only after the loop - so no preset could be the parent of
another preset loaded by the same pass. Only a parent loaded earlier (a system
preset, or one in the "base" subdirectory) resolved.

The drop was silent in the GUI. Since the CLI resolves --load-settings /
--load-filaments by matching the source file against the presets in the loaded
bundle, it turns into a hard failure there: a user preset inheriting another
user preset exits -5 with "Preset was not found in the loaded bundle".

Load in passes instead: a preset whose parent is not in the collection yet is
deferred and retried after the pass merges what it loaded, so each pass resolves
one more level of the hierarchy. A pass that resolves nothing reports the
remaining parents as missing, which also terminates an inheritance cycle. The
work list is sorted so the outcome does not depend on directory iteration order.
2026-09-09 18:02:41 +08:00
HanifKoh
8af92214d0 Extract and Unify Wipe Tower Estimation (#15532)
# Description

The pre-slice wipe tower size estimate existed twice:
`Print::wipe_tower_data()` (validation) and
`PartPlate::estimate_wipe_tower_size()` (GUI placement clamp, default
placement, preview, arrange, CLI placement) — with a third partial copy
in the CLI, which resolved the brim itself around the second. They were
hand-written twins reading their inputs from different places, so
validation could measure a tower with one number after the clamp had
placed it with another.

This PR extracts the estimate into one function,
`estimate_wipe_tower_footprint()` in
`src/libslic3r/GCode/WipeTowerEstimate.{hpp,cpp}`. It takes a
`ConfigBase&` (static `PrintConfig` and GUI/CLI `DynamicPrintConfig`
both work), the filament count, layer height and tallest object height,
and returns width, depth, height and the resolved brim width.
`Print::wipe_tower_data()` and a new
`PartPlate::estimate_wipe_tower_footprint()` become thin adapters around
it; `PartPlate::estimate_wipe_tower_size()` had no callers left and is
deleted.

**Inputs made to agree** — sharing the arithmetic is not enough when
each caller derives the inputs from its own view of the model:

* **Layer height** — thinnest layer among the objects on the plate,
resolved per object (Print used the first object's, PartPlate the
preset's).
* **Objects setting the height** — `PartPlate::get_extruders` counts an
object if *any* instance is on the plate, matching `PrintApply` (it only
looked at instance 0).
* **Height per object** — per on-plate instance, from the cached convex
hull (same z extent as the mesh). `PrintObject::size()` still measures
the model's first instance, so objects whose instances differ in scale
or x/y tilt can still disagree; that is inherent to the two data
sources.
* **Wipe tower filament** — counted for every caller, since
`Print::extruders()` adds it to the tool ordering even when unused.
* **Rib width cap** — kept for both (Print lacked it).
* **Config source** — everything read from the config passed in
(PartPlate read `m_print->config()`, stale on fresh plates and in the
CLI).
* **Dual-nozzle test** — `nozzle_diameter.size()` from the config for
both.

**One decision about whether a tower exists.** The rectangle branch
sized a tower the generator never builds while the rib branch reported
none for one it does; with rib as the shipped default, a single-filament
plate that still prints a tower (custom G-code tool changes) validated
against depth 0, collapsing the collision/exclusion hull to a point. The
purge volume is computed first, and an empty footprint is returned only
when nothing is purged, there is no tool change, and nothing else puts a
tower on the plate. The reason a single config cannot see arrives as a
resolved input: validation counts `Print::extruders(true)`.

A raft is deliberately **not** one of those reasons.
`DynamicPrintConfig::normalize_fdm_2` clears `enable_prime_tower` for a
plate that purges one filament unless smooth timelapse or wrapping
detection is on, and `Print::apply()` runs it, so a raft alone leaves no
tower to reserve for. (It also keeps the tower for a single *mixed*
filament, which this does not model — `Print::extruders(true)` does not
expand mixed filaments.)

**Two implementation notes:**

* Enums are read **by value**: a preset-built `DynamicPrintConfig` holds
`ConfigOptionEnumGeneric`, so a `dynamic_cast` to `ConfigOptionEnum<T>`
is null for exactly the config the GUI and CLI pass. The tests build
their configs the way `PresetBundle::full_config()` does, so that
storage is what gets tested.
* `PartPlate::estimate_wipe_tower_footprint()` is CLI-reachable, so
`get_extruders(bool)` gained a config-taking core with the identical
body; the GUI wrapper passes the app's presets, the adapter passes the
config it is given. `get_extruders_under_cli()` was not substituted: it
filters the plate's instances differently (skips unprintable ones, keeps
ones the plate flags as outside), so the GUI's filament set would have
changed in edge cases.

**Also fixed here:** `WipeTowerData` carries the effective width — set
by the estimate, and then by both planners at generation, so it never
disagrees with its neighbour `depth`; the preview and the containment
check take *whether there is a tower at all* from the footprint instead
of each re-deriving it; the config-taking `get_extruders` answers for an
object-less (`.gcode.3mf`) plate the way the wx overload does; the
preview takes body *and* brim from the plate's own footprint (an auto
brim drew every plate with the selected plate's brim);
`estimate_wipe_tower_polygon` builds its margin from the resolved brim
("Auto" gave a margin of 0) and no longer calls `std::clamp` with `hi <
lo`; the estimate falls back to declared defaults instead of hand-copied
constants. `estimate_wipe_tower_size()` /
`estimate_wipe_tower_polygon()` lose four parameters every caller took
from the same config.

## Behaviour changes reviewers should know about

G-code is never affected; no 3MF, profile or string changes. But this is
**not** a pure refactor:

1. **Validation now reserves what the placement clamp reserves**, which
is in places larger than before. A saved 3MF with a tower close to an
exclusion area or the rear edge can be rejected where it previously
sliced; dragging resolves it since the clamp agrees. Nothing re-clamps a
stored position on load (out of scope; the CLI side lands with #15518).
2. **`PartPlate::get_extruders` counts any-instance-on-plate**, which
reaches every caller of it, not only the estimate. It is `PrintApply`'s
rule and closes a GUI/CLI divergence.
3. **`estimate_wipe_tower_polygon`'s rear/right bound is looser by one
brim width** (it subtracted the brim twice).
4. **A single-filament plate with a rib wall no longer reserves a
phantom tower.**
5. **A single-filament plate whose tower comes from wrapping detection
is now validated against the bed.** Neither the old estimate (which read
only the wall type and smooth timelapse) nor the old containment gate
(the filament count or smooth timelapse) knew about that tower, so
between them it was never checked. It is printed, so it can be rejected
now.

Not addressed: the estimate still does not read `wipe_tower_type` or
per-filament `filament_prime_volume`, inherited unchanged from both
copies (the generated Type 1 tower is ~10 mm larger than the estimate on
Bambu profiles). #15516 mirrors the planners and folds into this
function on rebase.

## Verification

Before/after on the same fixtures with a main build and this branch, all
numbers read from the CLI (details, method and the real-tower and
arrange-clamp tables in the first comment):

| Fixture (divergence) | Side | Before (w × d, mm) | After (w × d, mm) |
|---|---|---|---|
| control | GUI/CLI · validation | 23.585 × 23.585 · 23.585 × 23.585 |
same |
| per-object layer 0.1 | GUI/CLI · validation | **23.585** · 31.637 |
**31.638** · 31.637 |
| unused `wipe_tower_filament` | GUI/CLI · validation depth | **39.332**
· 44.542 | **44.541** · 44.542 |
| tall object, instance 0 on another plate | GUI/CLI · validation |
**23.585** · 29.391 | **29.390** · 29.391 |
| rib cap binds | GUI/CLI · validation | 11.170 · **13.910** | 11.170 ·
**11.170** |

Before, the two estimates disagree on every divergence fixture; after,
they agree to the 0.001 mm bisection resolution, the control is
unchanged, and G-code is byte-identical. GUI screenshots of the preview
on both binaries are in the same comment.

The table was measured on the first commit; none of its fixtures uses a
raft or a zero purge volume, so the second commit does not move them.
G-code equivalence was re-checked on the final tip: `Cube.3mf` sliced by
a `main` build and by this branch is byte-identical.

# Screenshots/Recordings/Graphs
Before:
No Wipe Tower Preview:
<img width="2068" height="871" alt="image"
src="https://github.com/user-attachments/assets/7875a944-b8db-4bbc-b380-e8188a45caa7"
/>

After:
Has Wipe Tower Preview:
<img width="2551" height="882" alt="image"
src="https://github.com/user-attachments/assets/b4413695-4f24-4aa3-bae4-57304e8b7865"
/>

**Per-object layer height reaching the preview.** One object with a 0.1
mm override against a 0.2 mm preset. `main` sizes the previewed tower
from the preset, so it is smaller than the one validation reserves and
the one that prints; this PR sizes it from the object. Captured
headlessly on both builds from the same project, top view:

<img width="1408" height="596" alt="D_per_object_layer_height"
src="https://github.com/user-attachments/assets/989920fc-9f14-4658-8a3d-681c92a7f754"
/>

Measured over the four evidence fixtures on both builds, this is the
only one of the corrected inputs that changes what is drawn: the others
(an object contributing through a non-zero instance, an unused
`wipe_tower_filament`) change the estimate by amounts confirmed through
the CLI bisection above, but leave the rendered tower pixel-identical.
Arrange is unaffected either way — the tower enters the arranger as a
fixed obstacle (`m_unselected`), so it never moves.



## Tests

* `tests/libslic3r/test_wipe_tower_estimate.cpp` (10 cases / 104
assertions): rectangle and rib sizing, stability floor and auto brim,
single-filament cases (timelapse, wrapping, and a raft *not* reserving
one), a tool change reserving the floor when the purge volumes resolve
to zero, both wall types agreeing on tower existence, dual-nozzle
volume, the shipped flush-matrix path, default fallback for a missing
key, and a {rectangle, cone, rib} × {type1, type2} matrix asserting a
preset-shaped `DynamicPrintConfig` and a static `FullPrintConfig` give
the same footprint.
* `tests/fff_print/test_wipe_tower.cpp`: what `Print` feeds the
estimator — thinnest object layer height, effective width reaching
validation, the width staying current through generation, a
single-filament plate reserving a tower only when one is really printed
(raft no, smooth timelapse yes), and a wrapping-detection tower being
bed-validated. The last two fail on `main` and on the first commit of
this PR.
* Full suites green on this branch: `libslic3r_tests` 342 cases / 58325
assertions, `fff_print_tests` 174 cases / 3152 assertions. `--target
all` builds clean (including `OrcaSlicer_profile_validator`, which needs
`-DORCA_TOOLS=ON`). No new warnings.
* CLI evidence run above; its unused-`wipe_tower_filament` fixture is
also the regression check for the adapter under the CLI, which no unit
test can reach (`PartPlate` needs a GL context).

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-09 17:46:20 +08:00
4 changed files with 293 additions and 148 deletions

View File

@@ -1624,6 +1624,7 @@ void PresetCollection::reset(bool delete_files)
unlock();
m_map_alias_to_profile_name.clear();
m_map_system_profile_renamed.clear();
m_unresolved_parents.clear();
}
void PresetCollection::add_default_preset(const std::vector<std::string> &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &preset_name)
@@ -1676,178 +1677,218 @@ void PresetCollection::load_presets(
}
std::string errors_cummulative;
// Store the loaded presets into a new vector, otherwise the binary search for already existing presets would be broken.
// (see the "Preset already present, not loading" message).
std::deque<Preset> presets_loaded;
//BBS: get the extruder related info for this preset collection
std::string extruder_id_name, extruder_variant_name;
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
//BBS: change to json format
std::vector<boost::filesystem::path> pending;
for (auto &dir_entry : boost::filesystem::directory_iterator(dir))
{
std::string file_name = dir_entry.path().filename().string();
//if (Slic3r::is_ini_file(dir_entry)) {
if (Slic3r::is_json_file(file_name)) {
// Remove the .ini suffix.
std::string name = file_name.erase(file_name.size() - 5);
std::string canonical_name = this->canonical_preset_name(name, resolved_origin);
if (this->find_preset(canonical_name, false)) {
// This happens when there's is a preset (most likely legacy one) with the same name as a system preset
// that's already been loaded from a bundle.
BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name;
continue;
}
try {
Preset preset(m_type, canonical_name, false);
preset.bundle_id = resolved_origin.bundle_id;
preset.file = dir_entry.path().string();
// Load the preset file, apply preset values on top of defaults.
pending.emplace_back(dir_entry.path());
// The iteration order of directory_iterator is unspecified; sort so that the number of passes
// below, and the presets that survive them, do not depend on the filesystem.
std::sort(pending.begin(), pending.end());
size_t loaded_count = 0;
// A preset may inherit another preset from this same directory, but the presets loaded here
// become visible to find_preset2() only once they are merged into m_presets at the end of a
// pass. A preset whose parent has not been merged yet is therefore deferred and retried on a
// further pass instead of being dropped; each pass resolves one more level of the hierarchy.
// A pass that resolves nothing means the remaining parents genuinely do not exist (or form a
// cycle), and only then are they reported as errors.
while (!pending.empty()) {
// Store the loaded presets into a new vector, otherwise the binary search for already existing presets would be broken.
// (see the "Preset already present, not loading" message).
std::deque<Preset> presets_loaded;
// Preset file and the parent name it could not resolve yet.
std::vector<std::pair<boost::filesystem::path, std::string>> deferred;
//BBS: change to json format
for (const auto &preset_path : pending)
{
std::string file_name = preset_path.filename().string();
//if (Slic3r::is_ini_file(dir_entry)) {
if (Slic3r::is_json_file(file_name)) {
// Remove the .ini suffix.
std::string name = file_name.erase(file_name.size() - 5);
std::string canonical_name = this->canonical_preset_name(name, resolved_origin);
if (this->find_preset(canonical_name, false)) {
// This happens when there's is a preset (most likely legacy one) with the same name as a system preset
// that's already been loaded from a bundle.
BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name;
continue;
}
try {
fs::path idx_path(preset.file);
idx_path.replace_extension(".info");
if (fs::exists(idx_path)) {
preset.load_info(idx_path.string());
}
DynamicPrintConfig config;
//BBS: change to json format
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
std::map<std::string, std::string> key_values;
std::string reason;
ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
if (! config_substitutions.empty())
substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
if (!reason.empty()) {
Preset preset(m_type, canonical_name, false);
preset.bundle_id = resolved_origin.bundle_id;
preset.file = preset_path.string();
// Substitutions reported below are rolled back if this preset ends up deferred,
// so that a retried preset does not report them twice.
const size_t substitutions_before = substitutions.size();
// Load the preset file, apply preset values on top of defaults.
try {
fs::path idx_path(preset.file);
idx_path.replace_extension(".info");
if (fs::exists(idx_path)) {
preset.load_info(idx_path.string());
}
DynamicPrintConfig config;
//BBS: change to json format
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
std::map<std::string, std::string> key_values;
std::string reason;
ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
if (! config_substitutions.empty())
substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
if (!reason.empty()) {
fs::path file_path(preset.file);
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file;
++m_errors;
continue;
}
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
boost::optional<Semver> version = Semver::parse(version_str);
if (!version) continue;
preset.version = *version;
if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end())
preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID];
if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end())
preset.description = key_values[BBL_JSON_KEY_DESCRIPTION];
if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end())
preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false";
//Orca: find and use the inherit config as the base
Preset* inherit_preset = nullptr;
ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS);
// check inherits_config
if (inherits_config) {
ConfigOptionString * option_str = dynamic_cast<ConfigOptionString *> (inherits_config);
std::string inherits_value = option_str->value;
// Orca: try to find if the parent preset has been renamed
inherit_preset = this->find_preset2(inherits_value);
Preset::normalize_inherits(config, inherit_preset);
} else {
;
}
const Preset& default_preset = this->default_preset_for(config);
if (inherit_preset) {
preset.config = inherit_preset->config;
preset.filament_id = inherit_preset->filament_id;
extend_default_config_length(config, false, {});
preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
}
else {
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config);
if ((inherits_config2 && !inherits_config2->value.empty())) {
// The parent may be another preset of this same pass, not merged into
// m_presets yet. Retry once it is; only a pass that resolves nothing
// reports the parent as missing.
substitutions.resize(substitutions_before);
deferred.emplace_back(preset_path, inherits_config2->value);
continue;
}
// We support custom root preset now
// Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field.
preset.config = default_preset.config;
preset.config.apply(std::move(config));
extend_default_config_length(preset.config, true, default_preset.config);
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id;
Preset::normalize(preset.config);
// Report configuration fields, which are misplaced into a wrong group.
std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config);
if (!incorrect_keys.empty()) {
++m_errors;
BOOST_LOG_TRIVIAL(error)
<< "Error in a preset file: The preset \"" << preset.file
<< "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed";
}
if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) {
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset.config.option("compatible_printers", true));
if (compatible_printers && compatible_printers->values.empty()) {
size_t at_pos = name.find('@');
if (at_pos != std::string::npos && at_pos + 1 < name.length()) {
compatible_printers->values.push_back(name.substr(at_pos + 1));
if (!read_only)
preset.save(nullptr);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << name;
}
}
}
preset.loaded = true;
//BBS: add some workaround for previous incorrect settings
if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id))
preset.setting_id.clear();
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible;
// add alias for custom filament preset
set_custom_preset_alias(preset);
} catch (const std::ifstream::failure &err) {
++m_errors;
BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what();
fs::path file_path(preset.file);
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file;
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
} catch (const std::runtime_error &err) {
++m_errors;
continue;
BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what();
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
fs::path file_path(preset.file);
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
}
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
boost::optional<Semver> version = Semver::parse(version_str);
if (!version) continue;
preset.version = *version;
if (preset_loaded_fn != nullptr)
preset_loaded_fn(preset);
if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end())
preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID];
if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end())
preset.description = key_values[BBL_JSON_KEY_DESCRIPTION];
if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end())
preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false";
//Orca: find and use the inherit config as the base
Preset* inherit_preset = nullptr;
ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS);
// check inherits_config
if (inherits_config) {
ConfigOptionString * option_str = dynamic_cast<ConfigOptionString *> (inherits_config);
std::string inherits_value = option_str->value;
// Orca: try to find if the parent preset has been renamed
inherit_preset = this->find_preset2(inherits_value);
Preset::normalize_inherits(config, inherit_preset);
} else {
;
}
const Preset& default_preset = this->default_preset_for(config);
if (inherit_preset) {
preset.config = inherit_preset->config;
preset.filament_id = inherit_preset->filament_id;
extend_default_config_length(config, false, {});
preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
}
else {
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config);
if ((inherits_config2 && !inherits_config2->value.empty())) {
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent %1% for config %2%!")%inherits_config2->value %preset.file;
++m_errors;
continue;
}
// We support custom root preset now
// Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field.
preset.config = default_preset.config;
preset.config.apply(std::move(config));
extend_default_config_length(preset.config, true, default_preset.config);
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id;
Preset::normalize(preset.config);
// Report configuration fields, which are misplaced into a wrong group.
std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config);
if (!incorrect_keys.empty()) {
++m_errors;
BOOST_LOG_TRIVIAL(error)
<< "Error in a preset file: The preset \"" << preset.file
<< "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed";
}
if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) {
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset.config.option("compatible_printers", true));
if (compatible_printers && compatible_printers->values.empty()) {
size_t at_pos = name.find('@');
if (at_pos != std::string::npos && at_pos + 1 < name.length()) {
compatible_printers->values.push_back(name.substr(at_pos + 1));
if (!read_only)
preset.save(nullptr);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << name;
}
}
}
preset.loaded = true;
//BBS: add some workaround for previous incorrect settings
if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id))
preset.setting_id.clear();
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible;
// add alias for custom filament preset
set_custom_preset_alias(preset);
} catch (const std::ifstream::failure &err) {
++m_errors;
BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what();
fs::path file_path(preset.file);
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
presets_loaded.emplace_back(preset);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name;
} catch (const std::runtime_error &err) {
++m_errors;
BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what();
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
fs::path file_path(preset.file);
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
errors_cummulative += err.what();
errors_cummulative += "\n";
}
if (preset_loaded_fn != nullptr)
preset_loaded_fn(preset);
presets_loaded.emplace_back(preset);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name;
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
errors_cummulative += "\n";
}
}
if (presets_loaded.size() > 0)
m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end()));
sort_presets();
loaded_count += presets_loaded.size();
if (deferred.empty())
break;
if (presets_loaded.empty()) {
for (const auto &entry : deferred) {
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent %1% for config %2%!")%entry.second %entry.first.string();
m_unresolved_parents[entry.first.string()] = entry.second;
++m_errors;
}
break;
}
pending.clear();
for (auto &entry : deferred)
pending.emplace_back(std::move(entry.first));
}
if (presets_loaded.size() > 0)
m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end()));
sort_presets();
//BBS: add config related logs
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": loaded %1% presets from %2%, type %3%")%presets_loaded.size() %dir %Preset::get_type_string(m_type);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": loaded %1% presets from %2%, type %3%")%loaded_count %dir %Preset::get_type_string(m_type);
//this->select_preset(first_visible_idx());
if (! errors_cummulative.empty())
throw Slic3r::RuntimeError(errors_cummulative);
@@ -3308,6 +3349,16 @@ Preset* PresetCollection::find_preset(const std::string &name, bool first_visibl
return first_visible_if_not_found ? &this->first_visible() : nullptr;
}
std::string PresetCollection::unresolved_parent(const boost::filesystem::path &file) const
{
for (const auto &[dropped_file, parent] : m_unresolved_parents) {
boost::system::error_code ec;
if (boost::filesystem::equivalent(file, boost::filesystem::path(dropped_file), ec) && !ec)
return parent;
}
return {};
}
Preset* PresetCollection::find_preset2(const std::string& name, bool auto_match/* = true */)
{
auto preset = find_preset(name, false, true);

View File

@@ -2,6 +2,7 @@
#define slic3r_Preset_hpp_
#include <deque>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
@@ -728,6 +729,9 @@ public:
{
return const_cast<PresetCollection*>(this)->find_preset2(name, auto_match);
}
// Name of the parent that kept the preset file from loading, or empty if the file loaded
// (or was never seen). Lets a caller that fails to find a preset explain why it is missing.
std::string unresolved_parent(const boost::filesystem::path &file) const;
size_t first_visible_idx() const;
// Return the index of the first visible, compatible, system base preset
@@ -965,6 +969,8 @@ private:
// Orca: used for validation only
int m_errors = 0;
// Preset files dropped by load_presets() because their parent does not exist, keyed by file path.
std::map<std::string, std::string> m_unresolved_parents;
};
// Printer supports the FFF and SLA technologies, with different set of configuration values,

View File

@@ -506,6 +506,11 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
}
if (error == "Preset identity is ambiguous")
return false;
// The file was seen but dropped at load time; say so rather than reporting it as unknown.
if (const std::string parent = collection->unresolved_parent(source_path); !parent.empty()) {
error = "Preset was not loaded because its parent preset \"" + parent + "\" was not found";
return false;
}
if (!allow_source_manifest) {
error = "Preset was not found in the loaded bundle";
return false;

View File

@@ -39,6 +39,20 @@ void write_preset_with_inherits(const DynamicPrintConfig &default_config, const
config.save_to_json(file.string(), name, "User", "1.0.0");
}
// Write a user preset json holding only "inherits" plus the given overrides, the way a GUI-saved
// user preset stores its diff against its parent. Anything else is inherited at load time.
void write_sparse_preset(const fs::path &file, const std::string &name, const std::string &inherits,
const std::vector<std::pair<std::string, std::string>> &overrides)
{
DynamicPrintConfig config;
config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = inherits;
for (const auto &override_pair : overrides)
config.set_deserialize_strict(override_pair.first, override_pair.second);
fs::create_directories(file.parent_path());
config.save_to_json(file.string(), name, "User", "1.0.0");
}
// Add an in-memory preset (no file) with the given inherits value (empty => root preset).
Preset &add_inmemory_preset(PresetCollection &coll, const std::string &name, const std::string &inherits = {})
{
@@ -365,6 +379,75 @@ std::vector<std::string> &compatible_list(PresetCollection &coll, const std::str
} // namespace
TEST_CASE("A user preset inheriting a user preset from the same directory is loaded", "[Preset][Inherits][Regression]")
{
ScopedTemporaryDir temp_dir;
PresetBundle bundle;
// The parent sorts after the child, so the child is necessarily reached before its parent is
// in the collection - the case a single load pass cannot resolve.
const fs::path preset_dir = temp_dir.path() / PRESET_PRINT_NAME;
write_sparse_preset(preset_dir / "AA Child.json", "AA Child", "ZZ Root", {{"layer_height", "0.15"}});
write_sparse_preset(preset_dir / "ZZ Root.json", "ZZ Root", "", {{"layer_height", "0.3"}, {"top_shell_layers", "7"}});
PresetsConfigSubstitutions substitutions;
bundle.prints.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
REQUIRE(bundle.prints.find_preset("ZZ Root") != nullptr);
const Preset *child = bundle.prints.find_preset("AA Child");
REQUIRE(child != nullptr);
CHECK_FALSE(bundle.has_errors());
REQUIRE(bundle.prints.get_preset_parent(*child) != nullptr);
CHECK(bundle.prints.get_preset_parent(*child)->name == "ZZ Root");
// The child's own override wins, and what it does not override comes from the parent rather
// than from the collection defaults.
CHECK_THAT(child->config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.15, 1e-9));
CHECK(child->config.opt_int("top_shell_layers") == 7);
}
TEST_CASE("A preset whose parent exists nowhere is reported, not loaded", "[Preset][Inherits][Regression]")
{
ScopedTemporaryDir temp_dir;
PresetBundle bundle;
const fs::path orphan_file = temp_dir.path() / PRESET_PRINT_NAME / "Orphan.json";
write_sparse_preset(orphan_file, "Orphan", "No Such Parent", {{"layer_height", "0.15"}});
PresetsConfigSubstitutions substitutions;
bundle.prints.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
CHECK(bundle.prints.find_preset("Orphan") == nullptr);
CHECK(bundle.has_errors());
CHECK(bundle.prints.unresolved_parent(orphan_file) == "No Such Parent");
// Resolving the dropped file names the missing parent instead of reporting the file as unknown.
DynamicPrintConfig config;
std::string error;
CHECK_FALSE(bundle.resolve_preset_config(config, Preset::TYPE_PRINT, orphan_file.string(),
ForwardCompatibilitySubstitutionRule::Disable, error, false));
CHECK(error == "Preset was not loaded because its parent preset \"No Such Parent\" was not found");
}
TEST_CASE("Presets inheriting each other in a cycle are reported, not loaded", "[Preset][Inherits][Regression]")
{
ScopedTemporaryDir temp_dir;
PresetBundle bundle;
const fs::path preset_dir = temp_dir.path() / PRESET_PRINT_NAME;
write_sparse_preset(preset_dir / "Ping.json", "Ping", "Pong", {{"layer_height", "0.15"}});
write_sparse_preset(preset_dir / "Pong.json", "Pong", "Ping", {{"layer_height", "0.3"}});
PresetsConfigSubstitutions substitutions;
bundle.prints.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
CHECK(bundle.prints.find_preset("Ping") == nullptr);
CHECK(bundle.prints.find_preset("Pong") == nullptr);
CHECK(bundle.has_errors());
}
TEST_CASE("Renamed printer/process names are normalized into compatible lists on load", "[Preset][Rename]")
{
PresetBundle bundle;