mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 21:42:43 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64035de87e | ||
|
|
0956b4d8fe | ||
|
|
5514559feb | ||
|
|
d5cf1502c4 | ||
|
|
37e2b6c928 | ||
|
|
efc9f253ee | ||
|
|
292cf0095e | ||
|
|
5c635d5e50 | ||
|
|
5496883493 | ||
|
|
31eb8a2bd1 | ||
|
|
70247ad298 | ||
|
|
d4840901fc | ||
|
|
5f01f21661 | ||
|
|
ecbe1b1b90 | ||
|
|
ffb4f192c1 | ||
|
|
4373bc3697 |
@@ -0,0 +1,219 @@
|
||||
# Nightly parity checks from OrcaSlicer/orca-test-repo, kept out of the
|
||||
# per-build "Run external slicer regression tests" step because they take far
|
||||
# longer than that step's budget:
|
||||
# effect - the CLI override sweep's full effect stage: every landed option
|
||||
# re-sliced on its own to see whether it changes the G-code
|
||||
# harness - the GUI-vs-CLI parity harness (metrics only, never fails)
|
||||
# Both test the latest successful build_all.yml Linux AppImage from main, with
|
||||
# sources checked out at the commit that build was made from. Nothing here
|
||||
# gates a build or a PR.
|
||||
name: Parity Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# build_all.yml starts at 17:00 UTC and has finished by ~20:00
|
||||
- cron: "0 21 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_repo_ref:
|
||||
description: "orca-test-repo ref to run"
|
||||
required: false
|
||||
default: "main"
|
||||
build_branch:
|
||||
description: "branch whose latest successful build_all artifact to test"
|
||||
required: false
|
||||
default: "main"
|
||||
fixtures:
|
||||
description: "harness fixture ids, space-separated (empty = all)"
|
||||
required: false
|
||||
default: ""
|
||||
cli_presets:
|
||||
description: "harness lane C presets: flat = flatten inherits first, raw = leaf profile as-is"
|
||||
required: false
|
||||
default: "flat"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Find the build to test
|
||||
# Don't run scheduled checks on forks
|
||||
if: github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
run_id: ${{ steps.find.outputs.run_id }}
|
||||
head_sha: ${{ steps.find.outputs.head_sha }}
|
||||
steps:
|
||||
- id: find
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh run list --workflow build_all.yml \
|
||||
--branch "${{ inputs.build_branch || 'main' }}" \
|
||||
--status success --limit 1 --json databaseId,headSha \
|
||||
--jq '"run_id=\(.[0].databaseId)\nhead_sha=\(.[0].headSha)"' \
|
||||
>> "$GITHUB_OUTPUT"
|
||||
cat "$GITHUB_OUTPUT"
|
||||
|
||||
effect:
|
||||
name: Override sweep effect stage (shard ${{ matrix.shard }})
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# orca-test-repo's parity/effect_routing.json holds a 2-way split,
|
||||
# ~12.5 min a shard on this runner
|
||||
shard: [0, 1]
|
||||
steps:
|
||||
- &checkout-suite
|
||||
name: Check out the test suite
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: OrcaSlicer/orca-test-repo
|
||||
ref: ${{ inputs.test_repo_ref || 'main' }}
|
||||
path: orca-test-repo
|
||||
|
||||
# The AppImage ships only packed preset caches, so profiles and the CLI
|
||||
# option surface come from the sources the build was made from
|
||||
- &checkout-slicer
|
||||
name: Check out OrcaSlicer at the build's commit
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.build.outputs.head_sha }}
|
||||
path: slicer
|
||||
lfs: 'false'
|
||||
|
||||
- &extract-appimage
|
||||
name: Download and extract the Linux AppImage
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh run download "${{ needs.build.outputs.run_id }}" --dir appimage \
|
||||
--pattern "OrcaSlicer_Linux_ubuntu_2404*"
|
||||
appimage=$(find appimage -name "*.AppImage" ! -name "*aarch64*" | head -1)
|
||||
[ -n "$appimage" ] || { echo "no x86_64 AppImage in run ${{ needs.build.outputs.run_id }}"; exit 1; }
|
||||
chmod +x "$appimage"
|
||||
"$appimage" --appimage-extract > /dev/null
|
||||
# The bare binary cannot find the AppImage's bundled libraries; AppRun
|
||||
# sets them up and execs it, so exit codes and signals pass through
|
||||
[ -x squashfs-root/AppRun ] || { echo "no AppRun in the AppImage"; exit 1; }
|
||||
echo "ORCA_BIN=$PWD/squashfs-root/AppRun" >> "$GITHUB_ENV"
|
||||
echo "ORCA_SOURCE=$PWD/slicer" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install the AppImage's host runtime dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install suite dependencies
|
||||
run: pip install -r orca-test-repo/requirements.txt
|
||||
|
||||
- name: Run the override sweep with the full effect stage
|
||||
id: run
|
||||
continue-on-error: true
|
||||
working-directory: orca-test-repo
|
||||
run: |
|
||||
set -o pipefail
|
||||
# -rA keeps the per-stage summaries, which pytest otherwise swallows
|
||||
# for passing tests
|
||||
python -m pytest test_cli_overrides.py -c pytest.ini -v -rA \
|
||||
--effect-full --effect-shard ${{ matrix.shard }}/2 \
|
||||
--orca-bin "$ORCA_BIN" --orca-source "$ORCA_SOURCE" \
|
||||
2>&1 | tee ../sweep.log
|
||||
|
||||
- name: Publish job summary
|
||||
if: always()
|
||||
run: |
|
||||
{
|
||||
echo "## Override sweep effect stage, shard ${{ matrix.shard }}/2"
|
||||
echo "Build ${{ needs.build.outputs.head_sha }} (run ${{ needs.build.outputs.run_id }})"
|
||||
echo '```'
|
||||
grep -E "\[override sweep" sweep.log || echo "no stage summaries, see the log"
|
||||
grep -E "^=+ .*(passed|failed)" sweep.log | tail -1 || true
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload the override report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: override-report-shard${{ matrix.shard }}
|
||||
path: |
|
||||
orca-test-repo/.pytest_cache/override_report.json
|
||||
sweep.log
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
# The sweep step continues on error so the summary and report still get
|
||||
# published; this puts the failure back on the job
|
||||
- name: Fail the job if the sweep failed
|
||||
if: steps.run.outcome == 'failure'
|
||||
run: |
|
||||
echo "the override sweep failed, see the job summary and the uploaded report" >&2
|
||||
exit 1
|
||||
|
||||
harness:
|
||||
name: GUI-vs-CLI parity harness
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- *checkout-suite
|
||||
- *checkout-slicer
|
||||
- *extract-appimage
|
||||
|
||||
- name: Install display tooling and the AppImage's host runtime
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
xvfb xdotool imagemagick openbox mesa-utils \
|
||||
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
|
||||
|
||||
- name: Run the parity harness
|
||||
run: |
|
||||
set -euo pipefail
|
||||
fixtures=()
|
||||
for f in ${{ inputs.fixtures || '' }}; do
|
||||
fixtures+=(--fixture "$f")
|
||||
done
|
||||
# 2 GUI displays: ~1.5 cores peak / ~1.9 GB on this 4-vCPU runner,
|
||||
# and each fixture is fully isolated, so results match a serial run
|
||||
python3 orca-test-repo/parity/run_parity.py \
|
||||
--slicer-root "$ORCA_SOURCE" --bin "$ORCA_BIN" \
|
||||
--cli-presets "${{ inputs.cli_presets || 'flat' }}" \
|
||||
--gui-workers 2 --out "$PWD/parity-out" "${fixtures[@]}"
|
||||
|
||||
- name: Publish job summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f parity-out/report.md ]; then
|
||||
cat parity-out/report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "the harness produced no report, see the log" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Drop per-lane datadirs before upload
|
||||
if: always()
|
||||
run: rm -rf parity-out/*/seed parity-out/*/datadir-* || true
|
||||
|
||||
- name: Upload the scorecard and evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: parity-scorecard
|
||||
path: parity-out/
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
+9
-18
@@ -587,10 +587,15 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
|
||||
add_compile_options(-Wno-${w})
|
||||
endforeach ()
|
||||
|
||||
# Turn everything else into an error. Dependency headers are exempt because the SYSTEM
|
||||
# include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their diagnostics out,
|
||||
# apart from GCC's maybe-uninitialized, demoted below.
|
||||
add_compile_options(-Werror)
|
||||
# GCC is not built in CI, so don't throw errors CI won't catch.
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
add_compile_options(-Werror=return-type)
|
||||
else ()
|
||||
# Turn everything else into an error. Dependency headers are exempt because the
|
||||
# SYSTEM include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their
|
||||
# diagnostics out.
|
||||
add_compile_options(-Werror)
|
||||
endif ()
|
||||
|
||||
# Demoted. Remove a name once its category is cleared on every compiler.
|
||||
set(warnings_demoted)
|
||||
@@ -612,20 +617,6 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
|
||||
cast-function-type-mismatch
|
||||
)
|
||||
endif ()
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
list(APPEND warnings_demoted
|
||||
# maybe-uninitialized runs after inlining and reports inside boost/variant,
|
||||
# boost/tuple and the bundled clipper header even with -isystem.
|
||||
maybe-uninitialized
|
||||
|
||||
# array-bounds is reported once, where ConfigOptionVector::set_at inlines
|
||||
# into OrcaSlicer.cpp on a branch the preceding type test rules out.
|
||||
array-bounds
|
||||
|
||||
# template-id-cdtor is a GCC 14+ warning in the bundled Clipper2 headers.
|
||||
template-id-cdtor
|
||||
)
|
||||
endif ()
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
list(APPEND warnings_demoted
|
||||
# enum-constexpr-conversion is a Clang warning that defaults to an error,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Snapmaker",
|
||||
"version": "02.04.00.12",
|
||||
"version": "02.04.00.13",
|
||||
"force_update": "0",
|
||||
"description": "Snapmaker configurations",
|
||||
"machine_model_list": [
|
||||
|
||||
+5
-5
@@ -15,13 +15,13 @@
|
||||
"1"
|
||||
],
|
||||
"cool_plate_temp": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"cool_plate_temp_initial_layer": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"eng_plate_temp": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"eng_plate_temp_initial_layer": [
|
||||
"100"
|
||||
@@ -48,7 +48,7 @@
|
||||
"Polymaker"
|
||||
],
|
||||
"hot_plate_temp": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"hot_plate_temp_initial_layer": [
|
||||
"100"
|
||||
@@ -72,7 +72,7 @@
|
||||
"110.8"
|
||||
],
|
||||
"textured_plate_temp": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"textured_plate_temp_initial_layer": [
|
||||
"100"
|
||||
|
||||
+8
-8
@@ -15,16 +15,16 @@
|
||||
"1"
|
||||
],
|
||||
"cool_plate_temp": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"cool_plate_temp_initial_layer": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"eng_plate_temp": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"eng_plate_temp_initial_layer": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"fan_cooling_layer_time": [
|
||||
"12"
|
||||
@@ -51,10 +51,10 @@
|
||||
"Polymaker"
|
||||
],
|
||||
"hot_plate_temp": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"hot_plate_temp_initial_layer": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"300"
|
||||
@@ -81,10 +81,10 @@
|
||||
"110"
|
||||
],
|
||||
"textured_plate_temp": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"textured_plate_temp_initial_layer": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"filament_type": [
|
||||
"ABS"
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
""
|
||||
],
|
||||
"hot_plate_temp": [
|
||||
"110"
|
||||
"100"
|
||||
],
|
||||
"hot_plate_temp_initial_layer": [
|
||||
"105"
|
||||
"100"
|
||||
],
|
||||
"overhang_fan_speed": [
|
||||
"20"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
""
|
||||
],
|
||||
"hot_plate_temp": [
|
||||
"110"
|
||||
"100"
|
||||
],
|
||||
"hot_plate_temp_initial_layer": [
|
||||
"100"
|
||||
|
||||
+38
-6
@@ -1387,6 +1387,25 @@ int CLI::run(int argc, char **argv)
|
||||
if (downward_check_option)
|
||||
downward_check = downward_check_option->value;
|
||||
|
||||
// --export-settings - writes its JSON to stdout, so reject every action or transform that may write there
|
||||
// too (--info, --help, --orient, slicing and exporting). The allowed ones do nothing when nothing is
|
||||
// sliced or exported.
|
||||
if (std::find(m_actions.begin(), m_actions.end(), "export_settings") != m_actions.end() && m_config.opt_string("export_settings") == "-") {
|
||||
static const std::set<std::string> stdout_compatible = { "export_settings", "uptodate", "load_defaultfila", "min_save",
|
||||
"mtcpp", "mstpp", "no_check", "normative_check", "pipe" };
|
||||
for (const std::vector<std::string> *opt_keys : { &m_actions, &m_transforms }) {
|
||||
for (const std::string &opt_key : *opt_keys) {
|
||||
if (stdout_compatible.count(opt_key) == 0) {
|
||||
std::string flag = opt_key;
|
||||
std::replace(flag.begin(), flag.end(), '_', '-');
|
||||
boost::nowide::cerr << "--export-settings - cannot be combined with --" << flag << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool start_gui = m_actions.empty() && !downward_check;
|
||||
if (start_gui) {
|
||||
BOOST_LOG_TRIVIAL(info) << "no action, start gui directly" << std::endl;
|
||||
@@ -2010,19 +2029,21 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
};
|
||||
|
||||
auto resolve_preset = [&ensure_cli_preset_bundle](const std::string &file, DynamicPrintConfig &config,
|
||||
// One resolver for the whole run, so presets from the same vendor tree share its load.
|
||||
std::unique_ptr<PresetBundle> system_preset_resolver;
|
||||
auto resolve_preset = [&ensure_cli_preset_bundle, &system_preset_resolver](const std::string &file, DynamicPrintConfig &config,
|
||||
std::string &config_type, const std::string &config_from,
|
||||
bool probe_type, std::string &error) {
|
||||
const auto *inherits = config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS);
|
||||
if (!probe_type && (inherits == nullptr || inherits->value.empty()))
|
||||
return true;
|
||||
|
||||
std::unique_ptr<PresetBundle> source_bundle;
|
||||
PresetBundle *bundle = nullptr;
|
||||
bool allow_source_manifest = false;
|
||||
if (config_from == "system") {
|
||||
source_bundle = std::make_unique<PresetBundle>();
|
||||
bundle = source_bundle.get();
|
||||
if (!system_preset_resolver)
|
||||
system_preset_resolver = std::make_unique<PresetBundle>();
|
||||
bundle = system_preset_resolver.get();
|
||||
allow_source_manifest = true;
|
||||
} else {
|
||||
bundle = ensure_cli_preset_bundle(error);
|
||||
@@ -5348,7 +5369,7 @@ int CLI::run(int argc, char **argv)
|
||||
//skip this object due to be locked in plate
|
||||
ap.itemid = locked_aps.size();
|
||||
locked_aps.emplace_back(ap);
|
||||
boost::nowide::cout <<__FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5937,7 +5958,11 @@ int CLI::run(int argc, char **argv)
|
||||
//FIXME check for mixing the FFF / SLA parameters.
|
||||
// or better save fff_print_config vs. sla_print_config
|
||||
//m_print_config.save(m_config.opt_string("save"));
|
||||
m_print_config.save_to_json(m_config.opt_string(opt_key), std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION));
|
||||
const std::string &settings_file = m_config.opt_string(opt_key);
|
||||
if (settings_file == "-")
|
||||
m_print_config.save_to_json(boost::nowide::cout, "project_settings", "project", SoftFever_VERSION, /*replace_invalid_utf8=*/true);
|
||||
else
|
||||
m_print_config.save_to_json(settings_file, std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION));
|
||||
} else if (opt_key == "info") {
|
||||
// --info works on unrepaired model
|
||||
for (Model &model : m_models) {
|
||||
@@ -7715,6 +7740,13 @@ bool CLI::setup(int argc, char **argv)
|
||||
this->print_help();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Orca: resolve here, while the process is still in the directory the user invoked it from.
|
||||
// GUI_App's constructor moves the working directory to <data_dir>/log, long before the GUI
|
||||
// opens these files in post_init(), and a relative path would then resolve against that.
|
||||
for (std::string &input_file : m_input_files)
|
||||
input_file = resolve_cli_input_path(input_file);
|
||||
|
||||
// Parse actions and transform options.
|
||||
for (auto const &opt_key : opt_order) {
|
||||
if (cli_actions_config_def.has(opt_key))
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <algorithm>
|
||||
#include <assert.h>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <regex>
|
||||
@@ -1515,6 +1516,19 @@ std::optional<PluginCapabilityRef> parse_capability_ref(const std::string& value
|
||||
|
||||
//BBS: add json support
|
||||
void ConfigBase::save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const
|
||||
{
|
||||
// Serialize first: if that throws (invalid UTF-8), the existing file stays untouched.
|
||||
std::ostringstream ss;
|
||||
this->save_to_json(ss, name, from, version);
|
||||
boost::nowide::ofstream c;
|
||||
c.open(file, std::ios::out | std::ios::trunc);
|
||||
c << ss.str();
|
||||
c.close();
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
|
||||
}
|
||||
|
||||
void ConfigBase::save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8) const
|
||||
{
|
||||
json j;
|
||||
//record the headers
|
||||
@@ -1561,12 +1575,7 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
|
||||
j["plugins"] = unique_refs;
|
||||
}
|
||||
|
||||
boost::nowide::ofstream c;
|
||||
c.open(file, std::ios::out | std::ios::trunc);
|
||||
c << j.dump(1, '\t') << std::endl;
|
||||
c.close();
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
|
||||
os << j.dump(1, '\t', false, replace_invalid_utf8 ? json::error_handler_t::replace : json::error_handler_t::strict) << std::endl;
|
||||
}
|
||||
|
||||
void ConfigBase::save(const std::string &file) const
|
||||
|
||||
@@ -2825,6 +2825,9 @@ public:
|
||||
|
||||
//BBS: add json support
|
||||
void save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const;
|
||||
// Same document, written to a stream. Invalid UTF-8 in a string value throws nlohmann's type_error unless
|
||||
// replace_invalid_utf8 is set, which writes U+FFFD instead (for callers such as stdout with no handler).
|
||||
void save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8 = false) const;
|
||||
|
||||
// Rebuild the in-memory "plugins" manifest (the "name;uuid;capability" references the plugin
|
||||
// dispatchers consume) from the plugin-backed options via the registered resolver. save_to_json()
|
||||
|
||||
+23
-13
@@ -1595,6 +1595,25 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
|
||||
return sparse_infill_polylines;
|
||||
}
|
||||
|
||||
// Returns the filament id (1-based) the region is ironed with, or -1 when the
|
||||
// region is not ironed. AllSolid always irons. TopSurfaces and TopmostOnly need
|
||||
// either some top shells or, in spiral mode, more than one bottom shell, and
|
||||
// TopmostOnly additionally needs the layer to be the topmost one.
|
||||
int Layer::choose_ironing_extruder(const PrintRegionConfig &cfg,
|
||||
bool spiral_mode,
|
||||
bool is_topmost_layer)
|
||||
{
|
||||
if (cfg.ironing_type == IroningType::NoIroning)
|
||||
return -1;
|
||||
const bool gate = (cfg.ironing_type == IroningType::AllSolid)
|
||||
|| ((cfg.top_shell_layers > 0 || (spiral_mode && cfg.bottom_shell_layers > 1))
|
||||
&& (cfg.ironing_type == IroningType::TopSurfaces
|
||||
|| (cfg.ironing_type == IroningType::TopmostOnly && is_topmost_layer)));
|
||||
if (!gate)
|
||||
return -1;
|
||||
return cfg.top_surface_filament_id;
|
||||
}
|
||||
|
||||
// Create ironing extrusions over top surfaces.
|
||||
void Layer::make_ironing()
|
||||
{
|
||||
@@ -1664,19 +1683,10 @@ void Layer::make_ironing()
|
||||
if (! layerm->slices.empty()) {
|
||||
IroningParams ironing_params;
|
||||
const PrintRegionConfig &config = layerm->region().config();
|
||||
if (config.ironing_type != IroningType::NoIroning &&
|
||||
(config.ironing_type == IroningType::AllSolid ||
|
||||
((config.top_shell_layers > 0 || (this->object()->print()->config().spiral_mode && config.bottom_shell_layers > 1)) &&
|
||||
(config.ironing_type == IroningType::TopSurfaces ||
|
||||
(config.ironing_type == IroningType::TopmostOnly && layerm->layer()->upper_layer == nullptr))))) {
|
||||
if (config.outer_wall_filament_id == config.top_surface_filament_id || config.wall_loops == 0) {
|
||||
// Iron the whole face.
|
||||
ironing_params.extruder = config.top_surface_filament_id;
|
||||
} else {
|
||||
// Iron just the infill.
|
||||
ironing_params.extruder = config.top_surface_filament_id;
|
||||
}
|
||||
}
|
||||
ironing_params.extruder = Layer::choose_ironing_extruder(
|
||||
config,
|
||||
/*spiral_mode=*/this->object()->print()->config().spiral_mode,
|
||||
/*is_topmost_layer=*/layerm->layer()->upper_layer == nullptr);
|
||||
if (ironing_params.extruder != -1) {
|
||||
//TODO just_infill is currently not used.
|
||||
ironing_params.just_infill = false;
|
||||
|
||||
@@ -16,6 +16,7 @@ using LayerPtrs = std::vector<Layer*>;
|
||||
class LayerRegion;
|
||||
using LayerRegionPtrs = std::vector<LayerRegion*>;
|
||||
class PrintRegion;
|
||||
class PrintRegionConfig;
|
||||
class PrintObject;
|
||||
class Print;
|
||||
|
||||
@@ -200,6 +201,11 @@ public:
|
||||
FillAdaptive::Octree *support_fill_octree,
|
||||
FillLightning::Generator* lightning_generator) const;
|
||||
void make_ironing();
|
||||
// Returns the filament id (1-based) the region is ironed with, or -1 when the
|
||||
// region is not ironed.
|
||||
static int choose_ironing_extruder(const PrintRegionConfig &cfg,
|
||||
bool spiral_mode,
|
||||
bool is_topmost_layer);
|
||||
void make_contour_z(const sla::IndexedMesh &mesh);
|
||||
|
||||
void export_region_slices_to_svg(const char *path) const;
|
||||
|
||||
@@ -484,7 +484,7 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
|
||||
else if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem)
|
||||
compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable;
|
||||
|
||||
auto collection_for_type = [](PresetBundle &bundle, Preset::Type preset_type) -> PresetCollection * {
|
||||
auto collection_for_type = [](const PresetBundle &bundle, Preset::Type preset_type) -> const PresetCollection * {
|
||||
switch (preset_type) {
|
||||
case Preset::TYPE_PRINT: return &bundle.prints;
|
||||
case Preset::TYPE_FILAMENT: return &bundle.filaments;
|
||||
@@ -493,15 +493,15 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
|
||||
}
|
||||
};
|
||||
|
||||
PresetCollection *collection = collection_for_type(*this, type);
|
||||
const PresetCollection *collection = collection_for_type(*this, type);
|
||||
if (collection == nullptr) {
|
||||
error = "Unsupported preset type";
|
||||
return false;
|
||||
}
|
||||
|
||||
const boost::filesystem::path source_path = boost::filesystem::absolute(source_file).lexically_normal();
|
||||
auto find_loaded = [&](PresetBundle &bundle) -> const Preset * {
|
||||
PresetCollection *loaded_collection = collection_for_type(bundle, type);
|
||||
auto find_loaded = [&](const PresetBundle &bundle) -> const Preset * {
|
||||
const PresetCollection *loaded_collection = collection_for_type(bundle, type);
|
||||
const Preset *resolved = nullptr;
|
||||
for (const Preset &preset : loaded_collection->get_presets()) {
|
||||
if (preset.file.empty())
|
||||
@@ -549,30 +549,11 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
|
||||
continue;
|
||||
|
||||
try {
|
||||
PresetBundle library_bundle;
|
||||
const PresetBundle *base_bundle = nullptr;
|
||||
if (vendor_id != ORCA_FILAMENT_LIBRARY &&
|
||||
boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) {
|
||||
library_bundle.m_preserve_vendor_source_paths = true;
|
||||
library_bundle.load_vendor_configs_from_json(root_dir.string(), ORCA_FILAMENT_LIBRARY, LoadSystem,
|
||||
compatibility_rule, nullptr, false);
|
||||
if (library_bundle.error_count() != 0) {
|
||||
error = "OrcaFilamentLibrary contains invalid presets";
|
||||
return false;
|
||||
}
|
||||
base_bundle = &library_bundle;
|
||||
}
|
||||
|
||||
PresetBundle source_bundle;
|
||||
source_bundle.m_preserve_vendor_source_paths = true;
|
||||
source_bundle.load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem,
|
||||
compatibility_rule, base_bundle, false);
|
||||
if (source_bundle.error_count() != 0) {
|
||||
error = "Vendor bundle contains invalid presets";
|
||||
const PresetBundle *loaded = load_source_vendor(root_dir, vendor_id, compatibility_rule, error);
|
||||
if (loaded == nullptr)
|
||||
return false;
|
||||
}
|
||||
|
||||
const Preset *resolved = find_loaded(source_bundle);
|
||||
const Preset *resolved = find_loaded(*loaded);
|
||||
if (resolved == nullptr) {
|
||||
if (error.empty())
|
||||
error = "Source file is not an instantiated preset in its vendor manifest";
|
||||
@@ -591,6 +572,37 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
|
||||
return false;
|
||||
}
|
||||
|
||||
const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::path &root_dir,
|
||||
const std::string &vendor_id,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
||||
std::string &error)
|
||||
{
|
||||
auto key = std::make_tuple(root_dir.string(), vendor_id, compatibility_rule);
|
||||
if (auto it = m_source_vendor_bundles.find(key); it != m_source_vendor_bundles.end())
|
||||
return it->second.get();
|
||||
|
||||
// The library loads with no base of its own, so the tree a vendor inherits from
|
||||
// is the same one that resolves the library's own presets.
|
||||
const PresetBundle *library = nullptr;
|
||||
if (vendor_id != ORCA_FILAMENT_LIBRARY &&
|
||||
boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) {
|
||||
library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error);
|
||||
if (library == nullptr) {
|
||||
error = "OrcaFilamentLibrary contains invalid presets";
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
auto bundle = std::make_unique<PresetBundle>();
|
||||
bundle->m_preserve_vendor_source_paths = true;
|
||||
bundle->load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, compatibility_rule, library, false);
|
||||
if (bundle->error_count() != 0) {
|
||||
error = "Vendor bundle contains invalid presets";
|
||||
return nullptr;
|
||||
}
|
||||
return m_source_vendor_bundles.emplace(std::move(key), std::move(bundle)).first->second.get();
|
||||
}
|
||||
|
||||
bool PresetBundle::resolve_preset_config_type(DynamicPrintConfig &config, Preset::Type &type,
|
||||
const std::string &source_file,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <shared_mutex>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <array>
|
||||
@@ -652,6 +653,17 @@ private:
|
||||
bool m_generate_vendor_caches { false };
|
||||
bool m_preserve_vendor_source_paths { false };
|
||||
|
||||
// Vendor trees loaded by resolve_preset_config's manifest path, so every preset
|
||||
// resolved through this bundle shares one load per source root and vendor. The
|
||||
// filament library is one such tree, shared by every vendor under its root.
|
||||
std::map<std::tuple<std::string, std::string, ForwardCompatibilitySubstitutionRule>, std::unique_ptr<PresetBundle>>
|
||||
m_source_vendor_bundles;
|
||||
|
||||
const PresetBundle *load_source_vendor(const boost::filesystem::path &root_dir,
|
||||
const std::string &vendor_id,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
||||
std::string &error);
|
||||
|
||||
// Orca: validation only - flag any printer with two or more compatible
|
||||
// filament presets sharing one filament_id (ambiguous AMS subtype match).
|
||||
bool check_duplicate_filament_subtypes() const;
|
||||
|
||||
@@ -11916,7 +11916,7 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
||||
|
||||
def = this->add("export_settings", coString);
|
||||
def->label = L("Export Settings");
|
||||
def->tooltip = L("This exports settings to a file.");
|
||||
def->tooltip = L("This exports settings to a file. Use - to write them to stdout.");
|
||||
def->cli_params = "settings.json";
|
||||
def->set_default_value(new ConfigOptionString("output.json"));
|
||||
|
||||
|
||||
@@ -314,6 +314,9 @@ extern unsigned get_current_pid();
|
||||
std::string per_user_temp_id();
|
||||
// Per-user temp root under `base`; an empty `user_id` returns `base` unchanged.
|
||||
std::string per_user_temp_dir(const std::string &base, const std::string &user_id);
|
||||
// Completes a relative command line input path against the current working directory. Absolute
|
||||
// paths and custom open protocol URLs are returned unchanged.
|
||||
std::string resolve_cli_input_path(const std::string &path);
|
||||
// BBS: backup & restore
|
||||
std::string get_process_name(int pid);
|
||||
|
||||
|
||||
@@ -1339,6 +1339,19 @@ std::string per_user_temp_dir(const std::string &base, const std::string &user_i
|
||||
return base + "/orcaslicer_" + user_id;
|
||||
}
|
||||
|
||||
std::string resolve_cli_input_path(const std::string &path)
|
||||
{
|
||||
const boost::filesystem::path input(path);
|
||||
if (path.empty() || is_supported_open_protocol(path) || input.is_absolute())
|
||||
return path;
|
||||
|
||||
boost::system::error_code ec;
|
||||
const boost::filesystem::path resolved = boost::filesystem::system_complete(input, ec);
|
||||
if (ec)
|
||||
return path;
|
||||
return resolved.lexically_normal().make_preferred().string();
|
||||
}
|
||||
|
||||
// BBS: backup & restore
|
||||
std::string get_process_name(int pid)
|
||||
{
|
||||
|
||||
@@ -2098,12 +2098,6 @@ void GLCanvas3D::render(bool only_init)
|
||||
_render_selection_center();
|
||||
#endif // ENABLE_RENDER_SELECTION_CENTER
|
||||
|
||||
// we need to set the mouse's scene position here because the depth buffer
|
||||
// could be invalidated by the following gizmo render methods
|
||||
// this position is used later into on_mouse() to drag the objects
|
||||
if (m_picking_enabled)
|
||||
m_mouse.scene_position = _mouse_to_3d(m_mouse.position.cast<coord_t>());
|
||||
|
||||
// sidebar hints need to be rendered before the gizmos because the depth buffer
|
||||
// could be invalidated by the following gizmo render methods
|
||||
_render_selection_sidebar_hints();
|
||||
@@ -4491,12 +4485,13 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box();
|
||||
volume_bbox.offset(1.0);
|
||||
const bool is_cut_connector_selected = m_selection.is_any_connector();
|
||||
if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(m_mouse.scene_position) && !is_cut_connector_selected) {
|
||||
const Vec3d scene_position = _mouse_to_3d(pos);
|
||||
if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(scene_position) && !is_cut_connector_selected) {
|
||||
m_volumes.volumes[volume_idx]->hover = GLVolume::HS_None;
|
||||
// The dragging operation is initiated.
|
||||
m_mouse.drag.move_volume_idx = volume_idx;
|
||||
m_selection.setup_cache();
|
||||
m_mouse.drag.start_position_3D = m_mouse.scene_position;
|
||||
m_mouse.drag.start_position_3D = scene_position;
|
||||
m_sequential_print_clearance_first_displacement = true;
|
||||
m_moving = true;
|
||||
|
||||
|
||||
@@ -337,7 +337,6 @@ class GLCanvas3D
|
||||
|
||||
bool dragging{ false };
|
||||
Vec2d position{ DBL_MAX, DBL_MAX };
|
||||
Vec3d scene_position{ DBL_MAX, DBL_MAX, DBL_MAX };
|
||||
bool ignore_left_up{ false };
|
||||
Drag drag;
|
||||
bool ignore_right_up;
|
||||
|
||||
@@ -55,6 +55,16 @@ wxString web_base_url()
|
||||
return wxString("file://") + from_u8(dir) + "/";
|
||||
}
|
||||
|
||||
|
||||
// True when two consecutive main-frame URLs name the same document, i.e. they differ
|
||||
// only in the fragment. The MSW backend synthesises a wxEVT_WEBVIEW_LOADED for such
|
||||
// in-document navigation (a page setting location.hash), which must not be mistaken
|
||||
// for a reload; an identical URL is a genuine reload and is not fragment navigation.
|
||||
bool is_fragment_navigation(const wxString& from, const wxString& to)
|
||||
{
|
||||
return from != to && from.BeforeFirst('#') == to.BeforeFirst('#');
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
||||
@@ -139,19 +149,32 @@ void PluginWebDialog::destroy_for_plugin(PluginWebDialog* dialog)
|
||||
|
||||
void PluginWebDialog::on_bootstrap_event(wxWebViewEvent& event)
|
||||
{
|
||||
// The first bootstrap load (or its error) triggers the swap to plugin HTML;
|
||||
// the resulting plugin-page load is ignored (guarded by m_content_loaded).
|
||||
load_plugin_content();
|
||||
// The first bootstrap load (or its error) triggers the swap to plugin HTML. Once that
|
||||
// page has settled, a further main-frame load is a browser reload (context menu or
|
||||
// keyboard shortcut): the injected page has no URL of its own, so the reload fetches
|
||||
// the base URL instead and the plugin HTML has to be put back. In-document navigation
|
||||
// is not a reload and must be left alone, as is a post-load error, which only ever
|
||||
// means a failed subresource.
|
||||
if (!m_content_loaded)
|
||||
load_plugin_content();
|
||||
else if (event.GetEventType() == wxEVT_WEBVIEW_LOADED) {
|
||||
const wxString previous_url = m_last_url;
|
||||
m_last_url = event.GetURL();
|
||||
if (m_own_page_load)
|
||||
m_own_page_load = false;
|
||||
else if (!is_fragment_navigation(previous_url, m_last_url))
|
||||
load_plugin_content();
|
||||
}
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void PluginWebDialog::load_plugin_content()
|
||||
{
|
||||
if (m_content_loaded)
|
||||
return;
|
||||
m_content_loaded = true;
|
||||
if (wxWebView* wv = browser())
|
||||
if (wxWebView* wv = browser()) {
|
||||
m_own_page_load = true;
|
||||
wv->SetPage(wxString::FromUTF8(m_html), web_base_url());
|
||||
}
|
||||
}
|
||||
|
||||
void PluginWebDialog::on_script_message(const nlohmann::json& payload)
|
||||
|
||||
@@ -72,6 +72,8 @@ private:
|
||||
|
||||
std::string m_html;
|
||||
bool m_content_loaded{false};
|
||||
bool m_own_page_load{false}; // a SetPage of the plugin HTML is in flight
|
||||
wxString m_last_url; // URL of the last main-frame load, to spot fragment navigation
|
||||
bool m_open{true};
|
||||
bool m_close_fired{false};
|
||||
std::optional<nlohmann::json> m_result;
|
||||
|
||||
@@ -1037,9 +1037,6 @@ size_t PublishSettingsDialog::section_group_for(Section kind)
|
||||
section.mixed_tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style);
|
||||
section.mixed_tabs->SetFont(Label::Body_14);
|
||||
section.mixed_tabs->SetBackgroundColour(GetBackgroundColour());
|
||||
// The mixed tabs carry full swatch compositions: give them a touch more room than the
|
||||
// filament tabs so neighbouring compositions stay distinguishable (must precede AppendItem).
|
||||
section.mixed_tabs->SetItemSpace(FromDIP(3));
|
||||
page_sizer->Add(section.mixed_tabs, 0, wxEXPAND | wxTOP, FromDIP(2));
|
||||
section.mixed_tabs->Hide();
|
||||
}
|
||||
|
||||
@@ -311,8 +311,11 @@ void Button::render(wxDC& dc)
|
||||
}
|
||||
}
|
||||
auto szContent = textSize;
|
||||
// Whether the measured content reserved the text/icon gap. macOS measures an empty label
|
||||
// as 0-high, so the gap is skipped there; the dot must not advance past it in that case.
|
||||
const bool gap_reserved = szContent.y > 0;
|
||||
if (icon.bmp().IsOk()) {
|
||||
if (szContent.y > 0) {
|
||||
if (gap_reserved) {
|
||||
//BBS norrow size between text and icon
|
||||
if (vertical)
|
||||
szContent.y += spacing;
|
||||
@@ -357,10 +360,10 @@ void Button::render(wxDC& dc)
|
||||
dc.DrawBitmap(icon.bmp(), pt);
|
||||
//BBS norrow size between text and icon
|
||||
if (vertical) {
|
||||
pt.y += szIcon.y + spacing;
|
||||
pt.y += szIcon.y + (gap_reserved ? spacing : 0);
|
||||
pt.x = rcContent.x;
|
||||
} else {
|
||||
pt.x += szIcon.x + spacing;
|
||||
pt.x += szIcon.x + (gap_reserved ? spacing : 0);
|
||||
pt.y = rcContent.y;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ int TabCtrl::AppendItem(const wxString& item, int image, int selImage, void* cli
|
||||
btns.push_back(btn);
|
||||
if (btns.size() > 1)
|
||||
sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0});
|
||||
sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, item_space);
|
||||
sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL);
|
||||
sizer->AddStretchSpacer(1);
|
||||
relayout();
|
||||
return btns.size() - 1;
|
||||
@@ -256,12 +256,12 @@ void TabCtrl::relayout()
|
||||
int item = sel + 1;
|
||||
int first = 0;
|
||||
for (int i = 0; i < item; ++i)
|
||||
offset += btns[i]->GetMinSize().x + item_space * 2;
|
||||
offset += btns[i]->GetMinSize().x;
|
||||
if (item < btns.size())
|
||||
offset += btns[item]->GetMinSize().x + item_space * 2;
|
||||
offset += btns[item]->GetMinSize().x;
|
||||
int width = GetSize().x;
|
||||
for (int i = 0; i < btns.size(); ++i) {
|
||||
auto size = btns[i]->GetMinSize().x + item_space * 2;
|
||||
auto size = btns[i]->GetMinSize().x;
|
||||
if (i < sel && offset > width) {
|
||||
sizer->Show(i * 2 + 1, false);
|
||||
sizer->Show(i * 2 + 2, false);
|
||||
@@ -284,26 +284,17 @@ void TabCtrl::relayout()
|
||||
if (item >= btns.size())
|
||||
--item;
|
||||
// Keep spacing 2 ~ 10 TAB_BUTTON_SPACE
|
||||
int b = GetSize().x - offset - 10 - (item + 1 - first) * item_space * 8;
|
||||
int b = GetSize().x - offset - 10 - (item + 1 - first) * 16;
|
||||
sizer->GetItem(item * 2 + 2)->SetMinSize({b > 0 ? b : 0, 0});
|
||||
Layout();
|
||||
}
|
||||
|
||||
void TabCtrl::SetItemSpace(int space)
|
||||
{
|
||||
if (space < 0 || space == item_space)
|
||||
return;
|
||||
item_space = space;
|
||||
relayout();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
int TabCtrl::GetFullSize() const
|
||||
{
|
||||
// Mirrors relayout(): a 10px leading spacer plus every button's min width and spacing.
|
||||
// Mirrors relayout(): a 10px leading spacer plus every button's min width.
|
||||
int width = 10;
|
||||
for (const Button* btn : btns)
|
||||
width += btn->GetMinSize().x + item_space * 2;
|
||||
width += btn->GetMinSize().x;
|
||||
return width;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ class TabCtrl : public StaticBox
|
||||
|
||||
int sel = -1;
|
||||
wxFont bold;
|
||||
int item_space = 2; // space around each button, both sides (SetItemSpace)
|
||||
|
||||
public:
|
||||
TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0);
|
||||
@@ -64,10 +63,6 @@ public:
|
||||
int GetNextVisible(int item) const;
|
||||
bool IsVisible(unsigned int item) const;
|
||||
|
||||
// Extra space around each tab button (in px on both sides). Defaults to the control-wide
|
||||
// standard; call before appending items so every button picks it up.
|
||||
void SetItemSpace(int space);
|
||||
|
||||
int GetFullSize() const;
|
||||
|
||||
private:
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/Layer.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/SVG.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
@@ -676,6 +677,73 @@ TEST_CASE("Ironing follows the solid infill rotation template", "[Fill]")
|
||||
REQUIRE(compared > int(ironing.size()) / 2);
|
||||
}
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
PrintRegionConfig ironing_config(IroningType type,
|
||||
int top_surface_filament_id = 1,
|
||||
int top_shell_layers = 3,
|
||||
int bottom_shell_layers = 1)
|
||||
{
|
||||
PrintRegionConfig cfg;
|
||||
cfg.ironing_type.value = type;
|
||||
cfg.top_surface_filament_id.value = top_surface_filament_id;
|
||||
cfg.top_shell_layers.value = top_shell_layers;
|
||||
cfg.bottom_shell_layers.value = bottom_shell_layers;
|
||||
cfg.outer_wall_filament_id.value = 1;
|
||||
cfg.wall_loops.value = 2;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Ironing an all-solid region uses the top surface filament on every layer", "[Fill]")
|
||||
{
|
||||
const PrintRegionConfig cfg = ironing_config(IroningType::AllSolid, /*top_surface_filament_id=*/2);
|
||||
const bool is_topmost_layer = GENERATE(false, true);
|
||||
CAPTURE(is_topmost_layer);
|
||||
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, is_topmost_layer) == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("Ironing top surfaces uses the top surface filament when the region has top shells", "[Fill]")
|
||||
{
|
||||
const PrintRegionConfig cfg = ironing_config(IroningType::TopSurfaces,
|
||||
/*top_surface_filament_id=*/3,
|
||||
/*top_shell_layers=*/2);
|
||||
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("Ironing top surfaces without top shells needs spiral mode and more than one bottom shell", "[Fill]")
|
||||
{
|
||||
const PrintRegionConfig one_bottom_shell = ironing_config(IroningType::TopSurfaces,
|
||||
/*top_surface_filament_id=*/1,
|
||||
/*top_shell_layers=*/0,
|
||||
/*bottom_shell_layers=*/1);
|
||||
const PrintRegionConfig two_bottom_shells = ironing_config(IroningType::TopSurfaces,
|
||||
/*top_surface_filament_id=*/1,
|
||||
/*top_shell_layers=*/0,
|
||||
/*bottom_shell_layers=*/2);
|
||||
|
||||
REQUIRE(Layer::choose_ironing_extruder(two_bottom_shells, /*spiral_mode=*/true, /*is_topmost_layer=*/false) == 1);
|
||||
REQUIRE(Layer::choose_ironing_extruder(one_bottom_shell, /*spiral_mode=*/true, /*is_topmost_layer=*/false) == -1);
|
||||
REQUIRE(Layer::choose_ironing_extruder(two_bottom_shells, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == -1);
|
||||
}
|
||||
|
||||
TEST_CASE("Ironing the topmost surface only applies to the topmost layer", "[Fill]")
|
||||
{
|
||||
const PrintRegionConfig cfg = ironing_config(IroningType::TopmostOnly, /*top_surface_filament_id=*/4);
|
||||
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/true) == 4);
|
||||
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == -1);
|
||||
}
|
||||
|
||||
TEST_CASE("A region with ironing turned off is never ironed", "[Fill]")
|
||||
{
|
||||
const PrintRegionConfig cfg = ironing_config(IroningType::NoIroning);
|
||||
const bool spiral_mode = GENERATE(false, true);
|
||||
CAPTURE(spiral_mode);
|
||||
REQUIRE(Layer::choose_ironing_extruder(cfg, spiral_mode, /*is_topmost_layer=*/true) == -1);
|
||||
}
|
||||
|
||||
TEST_CASE("Solid infill direction offsets every layer when no template is set", "[Fill]")
|
||||
{
|
||||
auto angles_for = [](int direction) {
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
SCENARIO("Generic config validation performs as expected.", "[Config]") {
|
||||
@@ -488,6 +490,59 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[
|
||||
CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs);
|
||||
}
|
||||
|
||||
TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") {
|
||||
DynamicPrintConfig config;
|
||||
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
|
||||
config.set_key_value("wall_loops", new ConfigOptionInt(3));
|
||||
config.set_key_value("filament_type", new ConfigOptionStrings({ "PLA", "PETG" }));
|
||||
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28\nG1 Z5"));
|
||||
|
||||
ScopedTemporaryFile tmp(".json");
|
||||
config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0");
|
||||
std::string file_contents;
|
||||
{
|
||||
boost::nowide::ifstream ifs(tmp.string());
|
||||
file_contents.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||
}
|
||||
// The file format: one tab per nesting level and a trailing newline.
|
||||
REQUIRE_FALSE(file_contents.empty());
|
||||
CHECK(file_contents.rfind("{\n\t\"", 0) == 0);
|
||||
CHECK(file_contents.back() == '\n');
|
||||
|
||||
std::ostringstream strict, replaced;
|
||||
config.save_to_json(strict, "test_preset", "User", "1.0.0.0");
|
||||
config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true);
|
||||
CHECK(strict.str() == file_contents);
|
||||
CHECK(replaced.str() == file_contents);
|
||||
CHECK(nlohmann::json::parse(strict.str())["machine_start_gcode"] == "G28\nG1 Z5");
|
||||
}
|
||||
|
||||
TEST_CASE("save_to_json replaces invalid UTF-8 in a stream only when asked", "[Config]") {
|
||||
DynamicPrintConfig config;
|
||||
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff"));
|
||||
|
||||
std::ostringstream strict, replaced;
|
||||
CHECK_THROWS_AS(config.save_to_json(strict, "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error);
|
||||
REQUIRE_NOTHROW(config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true));
|
||||
CHECK(nlohmann::json::parse(replaced.str())["machine_start_gcode"] == "G28 ; \xEF\xBF\xBD");
|
||||
}
|
||||
|
||||
TEST_CASE("save_to_json leaves an existing file untouched when the config cannot be serialized", "[Config]") {
|
||||
DynamicPrintConfig config;
|
||||
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff"));
|
||||
|
||||
ScopedTemporaryFile tmp(".json");
|
||||
{
|
||||
boost::nowide::ofstream ofs(tmp.string());
|
||||
ofs << "previous";
|
||||
}
|
||||
CHECK_THROWS_AS(config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error);
|
||||
|
||||
boost::nowide::ifstream ifs(tmp.string());
|
||||
const std::string contents((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
|
||||
CHECK(contents == "previous");
|
||||
}
|
||||
|
||||
TEST_CASE("plugin capability references survive string-map serialization", "[Config][plugins]") {
|
||||
const std::vector<std::string> refs = {
|
||||
"master_plugin;;header-stamp",
|
||||
|
||||
@@ -987,6 +987,193 @@ TEST_CASE("Resolution terminates when no vendor manifest exists", "[Preset][Bund
|
||||
CHECK(error == "Preset was not found in the loaded bundle");
|
||||
}
|
||||
|
||||
TEST_CASE("Manifest-backed resolution reuses the vendor tree it already loaded", "[Preset][Bundle][Regression]")
|
||||
{
|
||||
ScopedTemporaryDir dir;
|
||||
const fs::path process_dir = dir.path() / "Acme" / "process";
|
||||
fs::create_directories(process_dir);
|
||||
std::ofstream((dir.path() / "Acme.json").string())
|
||||
<< R"({"version":"1.0.0","name":"Acme","process_list":[)"
|
||||
<< R"({"name":"fdm_process_common","sub_path":"process/base.json"},)"
|
||||
<< R"({"name":"Acme First","sub_path":"process/first.json"},)"
|
||||
<< R"({"name":"Acme Second","sub_path":"process/second.json"}]})";
|
||||
auto write_base = [&](double travel_speed) {
|
||||
std::ofstream((process_dir / "base.json").string())
|
||||
<< R"({"type":"process","name":"fdm_process_common","from":"system",)"
|
||||
<< R"("instantiation":"false","travel_speed":[")" << travel_speed << R"("]})";
|
||||
};
|
||||
auto write_child = [&](const std::string &file, const std::string &name) {
|
||||
std::ofstream((process_dir / file).string())
|
||||
<< R"({"type":"process","name":")" << name << R"(","from":"system",)"
|
||||
<< R"("instantiation":"true","inherits":"fdm_process_common"})";
|
||||
};
|
||||
write_base(111.0);
|
||||
write_child("first.json", "Acme First");
|
||||
write_child("second.json", "Acme Second");
|
||||
|
||||
auto travel_speed = [&](PresetBundle &bundle, const std::string &file) {
|
||||
DynamicPrintConfig raw;
|
||||
raw.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common";
|
||||
std::string error;
|
||||
REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, (process_dir / file).string(),
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||
return raw.option<ConfigOptionFloats>("travel_speed")->values.front();
|
||||
};
|
||||
|
||||
PresetBundle bundle;
|
||||
CHECK_THAT(travel_speed(bundle, "first.json"), Catch::Matchers::WithinAbs(111.0, 1e-6));
|
||||
|
||||
// Only a reload would see this change.
|
||||
write_base(222.0);
|
||||
CHECK_THAT(travel_speed(bundle, "second.json"), Catch::Matchers::WithinAbs(111.0, 1e-6));
|
||||
|
||||
PresetBundle fresh;
|
||||
CHECK_THAT(travel_speed(fresh, "second.json"), Catch::Matchers::WithinAbs(222.0, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("Manifest-backed resolution does not keep a vendor tree that failed to load", "[Preset][Bundle][Regression]")
|
||||
{
|
||||
ScopedTemporaryDir dir;
|
||||
const fs::path child_file = dir.path() / "Acme" / "process" / "child.json";
|
||||
auto write_manifest = [&](const std::string &leading_entry) {
|
||||
std::ofstream((dir.path() / "Acme.json").string())
|
||||
<< R"({"version":"1.0.0","name":"Acme","process_list":[)" << leading_entry
|
||||
<< R"({"name":"Acme Process","sub_path":"process/child.json"}]})";
|
||||
};
|
||||
write_manifest("123,");
|
||||
fs::create_directories(child_file.parent_path());
|
||||
std::ofstream(child_file.string())
|
||||
<< R"({"type":"process","name":"Acme Process","from":"system",)"
|
||||
<< R"("instantiation":"true","layer_height":"0.2"})";
|
||||
|
||||
PresetBundle bundle;
|
||||
auto resolve = [&](std::string &error) {
|
||||
DynamicPrintConfig raw;
|
||||
raw.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common";
|
||||
return bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, child_file.string(),
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, error);
|
||||
};
|
||||
|
||||
std::string error;
|
||||
CHECK_FALSE(resolve(error));
|
||||
CHECK_FALSE(error.empty());
|
||||
|
||||
write_manifest("");
|
||||
error.clear();
|
||||
CHECK(resolve(error));
|
||||
CHECK(error.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Manifest-backed resolution reuses the library base for type-probed files", "[Preset][Bundle][Regression]")
|
||||
{
|
||||
ScopedTemporaryDir dir;
|
||||
const fs::path library_pet = dir.path() / PresetBundle::ORCA_FILAMENT_LIBRARY / "filament" / "pet.json";
|
||||
const fs::path filament_dir = dir.path() / "Acme" / "filament";
|
||||
|
||||
std::ofstream((dir.path() / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json")).string())
|
||||
<< R"({"version":"1.0.0","name":"OrcaFilamentLibrary","filament_list":[)"
|
||||
<< R"({"name":"fdm_filament_pet","sub_path":"filament/pet.json","filament_id":"GFL99"}]})";
|
||||
fs::create_directories(library_pet.parent_path());
|
||||
auto write_library_pet = [&](double density) {
|
||||
std::ofstream(library_pet.string())
|
||||
<< R"({"type":"filament","name":"fdm_filament_pet","from":"system",)"
|
||||
<< R"("filament_id":"GFL99","instantiation":"false",)"
|
||||
<< R"("filament_type":["PETG"],"filament_density":[")" << density << R"("]})";
|
||||
};
|
||||
write_library_pet(1.27);
|
||||
|
||||
std::ofstream((dir.path() / "Acme.json").string())
|
||||
<< R"({"version":"1.0.0","name":"Acme","filament_list":[)"
|
||||
<< R"({"name":"Acme PETG","sub_path":"filament/petg.json","filament_id":"GFA00"},)"
|
||||
<< R"({"name":"Acme PETG Matte","sub_path":"filament/petg_matte.json","filament_id":"GFA01"}]})";
|
||||
fs::create_directories(filament_dir);
|
||||
auto write_child = [&](const std::string &file, const std::string &name, const std::string &filament_id) {
|
||||
std::ofstream((filament_dir / file).string())
|
||||
<< R"({"type":"filament","name":")" << name << R"(","from":"system",)"
|
||||
<< R"("filament_id":")" << filament_id << R"(","instantiation":"true","inherits":"fdm_filament_pet"})";
|
||||
};
|
||||
write_child("petg.json", "Acme PETG", "GFA00");
|
||||
write_child("petg_matte.json", "Acme PETG Matte", "GFA01");
|
||||
|
||||
auto density = [](const DynamicPrintConfig &config) {
|
||||
return config.option<ConfigOptionFloats>("filament_density")->values.front();
|
||||
};
|
||||
|
||||
PresetBundle bundle;
|
||||
DynamicPrintConfig first;
|
||||
first.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "fdm_filament_pet";
|
||||
std::string error;
|
||||
REQUIRE(bundle.resolve_preset_config(first, Preset::TYPE_FILAMENT, (filament_dir / "petg.json").string(),
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||
CHECK_THAT(density(first), Catch::Matchers::WithinAbs(1.27, 1e-6));
|
||||
|
||||
// Only a reload would see this change.
|
||||
write_library_pet(1.5);
|
||||
|
||||
DynamicPrintConfig second;
|
||||
Preset::Type type = Preset::TYPE_INVALID;
|
||||
REQUIRE(bundle.resolve_preset_config_type(second, type, (filament_dir / "petg_matte.json").string(),
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||
CHECK(type == Preset::TYPE_FILAMENT);
|
||||
CHECK_THAT(density(second), Catch::Matchers::WithinAbs(1.27, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("Manifest-backed resolution shares the library between vendors under one root", "[Preset][Bundle][Regression]")
|
||||
{
|
||||
ScopedTemporaryDir dir;
|
||||
const fs::path library_dir = dir.path() / PresetBundle::ORCA_FILAMENT_LIBRARY / "filament";
|
||||
|
||||
std::ofstream((dir.path() / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json")).string())
|
||||
<< R"({"version":"1.0.0","name":"OrcaFilamentLibrary","filament_list":[)"
|
||||
<< R"({"name":"fdm_filament_pet","sub_path":"filament/pet.json","filament_id":"GFL99"},)"
|
||||
<< R"({"name":"Generic PETG","sub_path":"filament/generic_petg.json","filament_id":"GFL98"}]})";
|
||||
fs::create_directories(library_dir);
|
||||
auto write_library_pet = [&](double density) {
|
||||
std::ofstream((library_dir / "pet.json").string())
|
||||
<< R"({"type":"filament","name":"fdm_filament_pet","from":"system",)"
|
||||
<< R"("filament_id":"GFL99","instantiation":"false",)"
|
||||
<< R"("filament_type":["PETG"],"filament_density":[")" << density << R"("]})";
|
||||
};
|
||||
write_library_pet(1.27);
|
||||
std::ofstream((library_dir / "generic_petg.json").string())
|
||||
<< R"({"type":"filament","name":"Generic PETG","from":"system",)"
|
||||
<< R"("filament_id":"GFL98","instantiation":"true","inherits":"fdm_filament_pet"})";
|
||||
|
||||
auto write_vendor = [&](const std::string &vendor, const std::string &filament_id) {
|
||||
const fs::path filament_dir = dir.path() / vendor / "filament";
|
||||
fs::create_directories(filament_dir);
|
||||
std::ofstream((dir.path() / (vendor + ".json")).string())
|
||||
<< R"({"version":"1.0.0","name":")" << vendor << R"(","filament_list":[)"
|
||||
<< R"({"name":")" << vendor << R"( PETG","sub_path":"filament/petg.json","filament_id":")" << filament_id << R"("}]})";
|
||||
std::ofstream((filament_dir / "petg.json").string())
|
||||
<< R"({"type":"filament","name":")" << vendor << R"( PETG","from":"system",)"
|
||||
<< R"("filament_id":")" << filament_id << R"(","instantiation":"true","inherits":"fdm_filament_pet"})";
|
||||
return filament_dir / "petg.json";
|
||||
};
|
||||
const fs::path acme_petg = write_vendor("Acme", "GFA00");
|
||||
const fs::path beta_petg = write_vendor("Beta", "GFB00");
|
||||
|
||||
auto density = [&](PresetBundle &bundle, const fs::path &file) {
|
||||
DynamicPrintConfig raw;
|
||||
raw.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "fdm_filament_pet";
|
||||
std::string error;
|
||||
REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_FILAMENT, file.string(),
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||
return raw.option<ConfigOptionFloats>("filament_density")->values.front();
|
||||
};
|
||||
|
||||
PresetBundle bundle;
|
||||
CHECK_THAT(density(bundle, acme_petg), Catch::Matchers::WithinAbs(1.27, 1e-6));
|
||||
|
||||
// Only a reload would see this change.
|
||||
write_library_pet(1.5);
|
||||
CHECK_THAT(density(bundle, beta_petg), Catch::Matchers::WithinAbs(1.27, 1e-6));
|
||||
CHECK_THAT(density(bundle, library_dir / "generic_petg.json"), Catch::Matchers::WithinAbs(1.27, 1e-6));
|
||||
|
||||
PresetBundle fresh;
|
||||
CHECK_THAT(density(fresh, beta_petg), Catch::Matchers::WithinAbs(1.5, 1e-6));
|
||||
}
|
||||
|
||||
// Orca: a filament in the Orca Filament Library that names its compatible printers has to hide the generic
|
||||
// library filament sharing its alias, the same way a vendor owned filament does. Otherwise both are compatible
|
||||
// with that printer and the plater combo box lists the shared alias twice.
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
@@ -88,3 +90,65 @@ TEST_CASE("copy_file reports the OS error when the destination cannot be written
|
||||
REQUIRE(std::all_of(code.begin(), code.end(), [](unsigned char c) { return std::isdigit(c) != 0; }));
|
||||
#endif // _WIN32
|
||||
}
|
||||
|
||||
TEST_CASE("A resolved input path still names the same file after the working directory changes", "[utils]") {
|
||||
ScopedTemporaryFile model(".3mf");
|
||||
{ std::ofstream out(model.string()); out << "3mf"; }
|
||||
const std::string name = model.path().filename().string();
|
||||
|
||||
// Resolve the bare name from the directory holding the file, then move away from it. The guard
|
||||
// restores the directory the test started in, wherever this leaves it.
|
||||
ScopedWorkingDirectory cwd(model.path().parent_path());
|
||||
const std::string resolved = resolve_cli_input_path(name);
|
||||
boost::filesystem::current_path(boost::filesystem::path(TEST_DATA_DIR));
|
||||
|
||||
REQUIRE(boost::filesystem::exists(resolved));
|
||||
REQUIRE(boost::filesystem::equivalent(resolved, model.path()));
|
||||
// Control: the bare name finds nothing from here, so resolving it this late would have failed.
|
||||
REQUIRE_FALSE(boost::filesystem::exists(name));
|
||||
}
|
||||
|
||||
TEST_CASE("resolve_cli_input_path completes a relative path against the working directory", "[utils]") {
|
||||
ScopedWorkingDirectory cwd(boost::filesystem::temp_directory_path());
|
||||
// Read back rather than reusing temp_directory_path(): changing to it resolves any symlink.
|
||||
const boost::filesystem::path here = boost::filesystem::current_path();
|
||||
|
||||
SECTION("a bare name") {
|
||||
REQUIRE(resolve_cli_input_path("model.3mf") == (here / "model.3mf").make_preferred().string());
|
||||
}
|
||||
SECTION("a ./ prefix is dropped") {
|
||||
REQUIRE(resolve_cli_input_path("./model.3mf") == (here / "model.3mf").make_preferred().string());
|
||||
}
|
||||
SECTION("a ../ traversal is collapsed") {
|
||||
REQUIRE(resolve_cli_input_path("../model.3mf") == (here.parent_path() / "model.3mf").make_preferred().string());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("resolve_cli_input_path leaves inputs that must not be completed unchanged", "[utils]") {
|
||||
SECTION("an absolute path") {
|
||||
const boost::filesystem::path absolute = (boost::filesystem::temp_directory_path() / "model.3mf").make_preferred();
|
||||
REQUIRE(resolve_cli_input_path(absolute.string()) == absolute.string());
|
||||
}
|
||||
#ifdef _WIN32
|
||||
// Every absolute form Windows accepts opens today, so each must come back byte for byte:
|
||||
// normalizing them would rewrite the forward slashes and rebuild the \\?\ and UNC prefixes.
|
||||
SECTION("an absolute Windows path of any form") {
|
||||
for (const std::string absolute : {R"(C:\models\model.3mf)",
|
||||
R"(C:/models/model.3mf)",
|
||||
R"(\\server\share\model.3mf)",
|
||||
R"(\\?\C:\models\model.3mf)"})
|
||||
REQUIRE(resolve_cli_input_path(absolute) == absolute);
|
||||
}
|
||||
#endif
|
||||
// These are downloaded rather than opened, and completing one would produce a path, not a URL.
|
||||
SECTION("a custom open protocol URL") {
|
||||
for (const std::string url : {"orcaslicer://open/?file=https://example.com/model.3mf",
|
||||
"prusaslicer://open/?file=https://example.com/model.3mf",
|
||||
"bambustudio://open/?file=https://example.com/model.3mf",
|
||||
"cura://open/?file=https://example.com/model.3mf"})
|
||||
REQUIRE(resolve_cli_input_path(url) == url);
|
||||
}
|
||||
SECTION("an empty argument") {
|
||||
REQUIRE(resolve_cli_input_path("").empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,4 +176,22 @@ inline void write_debug_stream([[maybe_unused]] const std::string &name, [[maybe
|
||||
#endif
|
||||
}
|
||||
|
||||
// Changes the working directory and restores the previous one on scope exit, including when an
|
||||
// assertion throws. It is process wide state shared with every other test.
|
||||
class ScopedWorkingDirectory
|
||||
{
|
||||
public:
|
||||
explicit ScopedWorkingDirectory(const boost::filesystem::path &dir)
|
||||
: m_previous(boost::filesystem::current_path())
|
||||
{
|
||||
boost::filesystem::current_path(dir);
|
||||
}
|
||||
~ScopedWorkingDirectory() { boost::system::error_code ec; boost::filesystem::current_path(m_previous, ec); }
|
||||
ScopedWorkingDirectory(const ScopedWorkingDirectory &) = delete;
|
||||
ScopedWorkingDirectory &operator=(const ScopedWorkingDirectory &) = delete;
|
||||
|
||||
private:
|
||||
boost::filesystem::path m_previous;
|
||||
};
|
||||
|
||||
#endif // SLIC3R_TEST_UTILS
|
||||
|
||||
Reference in New Issue
Block a user