mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caf74b1e39 | ||
|
|
a4c55c9976 | ||
|
|
ca668a3bc9 | ||
|
|
6b0e190e64 | ||
|
|
8effa27f4a | ||
|
|
72774e5398 | ||
|
|
ee1b845746 | ||
|
|
61b865706f | ||
|
|
55cc95d122 | ||
|
|
277ff35325 |
@@ -85,6 +85,15 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}"
|
||||
# clang-cl refuses a precompiled header from another cl.exe build and ccache
|
||||
# does not hash that build, so each one gets its own cache. The build number
|
||||
# is read from cl.exe itself; the toolset directory keeps its name across patches.
|
||||
if [ "${{ runner.os }}" = Windows ]; then
|
||||
vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
|
||||
toolset=$(tr -d '\r\n' < "$("$vswhere" -latest -products '*' -find 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt' | tr -d '\r')")
|
||||
cl=$("$vswhere" -latest -products '*' -find 'VC\Tools\MSVC\'"$toolset"'\**\cl.exe' | tr -d '\r' | head -1)
|
||||
leg="$leg-vc$("$cl" 2>&1 | grep -o -E 'Version [0-9.]+' | cut -d' ' -f2)"
|
||||
fi
|
||||
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
|
||||
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
|
||||
|
||||
|
||||
+76
-13
@@ -8,7 +8,7 @@ SCRIPT_PATH=$(dirname "$(readlink -f "${0}")")
|
||||
pushd "${SCRIPT_PATH}" > /dev/null
|
||||
|
||||
function usage() {
|
||||
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-p][-r][-s][-t][-u][-l][-L]"
|
||||
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-p][-r][-s][-t][-u][-l][-L [lld|mold]]"
|
||||
echo " -1: limit builds to one core (where possible)"
|
||||
echo " -j N: limit builds to N cores (where possible)"
|
||||
echo " -b: build in Debug mode"
|
||||
@@ -27,7 +27,7 @@ function usage() {
|
||||
echo " -t: build tests (optional), requires -s flag"
|
||||
echo " -u: install system dependencies (asks for sudo password; build prerequisite)"
|
||||
echo " -l: use Clang instead of GCC (default: GCC)"
|
||||
echo " -L: use ld.lld as linker (if available)"
|
||||
echo " -L [lld|mold]: use an alternate linker (if available) (default: lld)"
|
||||
echo "For a first use, you want to './${SCRIPT_NAME} -u'"
|
||||
echo " and then './${SCRIPT_NAME} -dsi'"
|
||||
echo "For a GitHub Actions-like Linux build locally, use './${SCRIPT_NAME} -g -istrlL'"
|
||||
@@ -115,8 +115,24 @@ while getopts ":1j:bcCdDeFghiprstulL" opt ; do
|
||||
FORWARDED_ARGS+=("-l")
|
||||
;;
|
||||
L )
|
||||
USE_LLD="1"
|
||||
FORWARDED_ARGS+=("-L")
|
||||
# -L takes an optional argument. getopts has no native support for
|
||||
# this, so L is declared bare (no ':') in the optstring above, and we
|
||||
# manually peek at the next unconsumed argv token via ${!OPTIND}. If
|
||||
# it's a bare 'lld' or 'mold' (not another option, i.e. doesn't start
|
||||
# with '-'), consume it as the explicit choice and advance OPTIND so
|
||||
# getopts doesn't reprocess it as a new flag. Otherwise, leave it
|
||||
# alone (it isn't meant for -L) and default to lld.
|
||||
LINKER_NAME="lld"
|
||||
next_arg="${!OPTIND-}"
|
||||
if [[ -n "${next_arg}" ]] && [[ "${next_arg}" != -* ]] ; then
|
||||
case "${next_arg}" in
|
||||
lld|mold )
|
||||
LINKER_NAME="${next_arg}"
|
||||
OPTIND=$((OPTIND + 1))
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
FORWARDED_ARGS+=("-L" "${LINKER_NAME}")
|
||||
;;
|
||||
* )
|
||||
echo "Unknown argument '${opt}', aborting."
|
||||
@@ -130,11 +146,23 @@ if [ ${OPTIND} -eq 1 ] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
shift $((OPTIND - 1))
|
||||
if [ $# -ne 0 ] ; then
|
||||
echo "Unknown argument '$1', aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${CLEAN_DOCKER_IMAGE}" ]] && [[ -z "${USE_DOCKER}" ]] ; then
|
||||
echo "Error: -F requires -g."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${USE_DOCKER}" ]] && [[ "${LINKER_NAME}" == "mold" ]] ; then
|
||||
echo "Error: -L mold is not available in the Docker/Podman build image, so -g and -L mold cannot be combined."
|
||||
echo "Omit -L mold when using -g (the container build defaults to GCC without mold), or drop -g and build with -L mold directly on a host with mold installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function check_available_memory_and_disk() {
|
||||
FREE_MEM_GB=$(free --gibi --total | grep 'Mem' | rev | cut --delimiter=" " --fields=1 | rev)
|
||||
MIN_MEM_GB=10
|
||||
@@ -492,14 +520,49 @@ if [[ -n "${USE_CLANG}" ]] ; then
|
||||
export CMAKE_C_CXX_COMPILER_CLANG=(-DCMAKE_C_COMPILER=/usr/bin/clang -DCMAKE_CXX_COMPILER=/usr/bin/clang++)
|
||||
fi
|
||||
|
||||
# Configure use of ld.lld as the linker when requested
|
||||
export CMAKE_LLD_LINKER_ARGS=()
|
||||
if [[ -n "${USE_LLD}" ]] ; then
|
||||
if command -v ld.lld >/dev/null 2>&1 ; then
|
||||
LLD_BIN=$(command -v ld.lld)
|
||||
export CMAKE_LLD_LINKER_ARGS=(-DCMAKE_LINKER="${LLD_BIN}" -DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld -DCMAKE_SHARED_LINKER_FLAGS=-fuse-ld=lld -DCMAKE_MODULE_LINKER_FLAGS=-fuse-ld=lld)
|
||||
# Configure use of an alternate linker (-L lld or -L mold) when requested
|
||||
export CMAKE_LINKER_ARGS=()
|
||||
if [[ -n "${LINKER_NAME}" ]] ; then
|
||||
case "${LINKER_NAME}" in
|
||||
lld )
|
||||
LINKER_BIN_NAME="ld.lld"
|
||||
;;
|
||||
mold )
|
||||
LINKER_BIN_NAME="mold"
|
||||
# -fuse-ld=mold requires GCC 12.1+. Older GCC (e.g. GCC 11, shipped
|
||||
# for Ubuntu 22.x via scripts/linux.d/debian) doesn't understand the
|
||||
# flag, and cmake's compiler check then fails with a confusing
|
||||
# generic "is not able to compile a simple test program" error
|
||||
# instead of naming the real cause. Catch it here instead.
|
||||
if [[ -z "${USE_CLANG}" ]] ; then
|
||||
GCC_BIN="${CC:-gcc}"
|
||||
if ! command -v "${GCC_BIN}" >/dev/null 2>&1 ; then
|
||||
GCC_BIN="cc"
|
||||
fi
|
||||
if command -v "${GCC_BIN}" >/dev/null 2>&1 ; then
|
||||
GCC_VERSION=$("${GCC_BIN}" -dumpfullversion 2>/dev/null)
|
||||
if [[ -n "${GCC_VERSION}" ]] && [[ "$(printf '%s\n%s\n' "${GCC_VERSION}" "12.1" | sort -V | head -n1)" != "12.1" ]] ; then
|
||||
echo "Error: -L mold requires GCC 12.1 or newer to support -fuse-ld=mold (found GCC ${GCC_VERSION} via '${GCC_BIN}')."
|
||||
echo "Use -l to build with Clang instead, upgrade your GCC toolchain, or omit -L mold."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v "${LINKER_BIN_NAME}" >/dev/null 2>&1 ; then
|
||||
LINKER_BIN=$(command -v "${LINKER_BIN_NAME}")
|
||||
export CMAKE_LINKER_ARGS=(-DCMAKE_LINKER="${LINKER_BIN}" "-DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=${LINKER_NAME}" "-DCMAKE_SHARED_LINKER_FLAGS=-fuse-ld=${LINKER_NAME}" "-DCMAKE_MODULE_LINKER_FLAGS=-fuse-ld=${LINKER_NAME}")
|
||||
else
|
||||
echo "Error: ld.lld not found. Please install the 'lld' package (e.g., sudo apt install lld) or omit -L."
|
||||
case "${LINKER_NAME}" in
|
||||
lld )
|
||||
echo "Error: ld.lld not found. Please install the 'lld' package (e.g., sudo apt install lld) or omit -L lld."
|
||||
;;
|
||||
mold )
|
||||
echo "Error: mold not found. Please install the 'mold' package or omit -L mold."
|
||||
;;
|
||||
esac
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -536,7 +599,7 @@ if [[ -n "${BUILD_DEPS}" ]] ; then
|
||||
BUILD_ARGS+=(-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}")
|
||||
fi
|
||||
|
||||
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
|
||||
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
|
||||
print_and_run cmake --build deps/$BUILD_DIR -j1
|
||||
fi
|
||||
|
||||
@@ -556,7 +619,7 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
|
||||
BUILD_ARGS+=(-DORCA_UPDATER_SIG_KEY="${ORCA_UPDATER_SIG_KEY}")
|
||||
fi
|
||||
|
||||
print_and_run cmake -S . -B $BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G "Ninja Multi-Config" \
|
||||
print_and_run cmake -S . -B $BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G "Ninja Multi-Config" \
|
||||
-DSLIC3R_PCH=${SLIC3R_PRECOMPILED_HEADERS} \
|
||||
-DORCA_TOOLS=ON \
|
||||
"${COLORED_OUTPUT}" \
|
||||
|
||||
Vendored
+8
@@ -27,8 +27,15 @@ endif ()
|
||||
# Boost.Container's bundled dlmalloc passes int* where the Win32 Interlocked API
|
||||
# takes volatile long*; cl compiles that with a warning, clang errors out.
|
||||
set(_boost_c_flags_line "")
|
||||
set(_boost_cxx_flags_line "")
|
||||
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
|
||||
set(_boost_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-incompatible-pointer-types")
|
||||
# The Visual Studio generator applies only the link language's flags to a
|
||||
# project, and boost_container links as C++, so its C file never sees
|
||||
# CMAKE_C_FLAGS. The C++ flags reach every file; keep CMake's defaults.
|
||||
if (CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
set(_boost_cxx_flags_line "-DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS} -Wno-incompatible-pointer-types")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
orcaslicer_add_cmake_project(Boost
|
||||
@@ -46,6 +53,7 @@ orcaslicer_add_cmake_project(Boost
|
||||
"${_context_arch_line}"
|
||||
"${_context_impl_line}"
|
||||
"${_boost_c_flags_line}"
|
||||
"${_boost_cxx_flags_line}"
|
||||
)
|
||||
|
||||
set(DEP_Boost_DEPENDS ZLIB)
|
||||
|
||||
Vendored
+5
@@ -184,6 +184,11 @@ function(orcaslicer_add_cmake_project projectname)
|
||||
|
||||
if (_dep_msvc_gen)
|
||||
set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}")
|
||||
# The toolset picks the compiler here, not the CMAKE_<LANG>_COMPILER
|
||||
# forwarded below, so without it a clang-cl superbuild builds with cl.
|
||||
if (CMAKE_GENERATOR_TOOLSET)
|
||||
list(APPEND _gen CMAKE_GENERATOR_TOOLSET "${CMAKE_GENERATOR_TOOLSET}")
|
||||
endif ()
|
||||
else()
|
||||
set(_gen "")
|
||||
endif()
|
||||
|
||||
Vendored
+3
@@ -7,4 +7,7 @@ orcaslicer_add_cmake_project(Draco
|
||||
${_options}
|
||||
URL https://github.com/google/draco/archive/refs/tags/1.5.7.zip
|
||||
URL_HASH SHA256=27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77
|
||||
CMAKE_ARGS
|
||||
# The encoder and decoder tools duplicate draco.lib; see deps-windows.cmake.
|
||||
"${DEP_LLD_FORCE_MULTIPLE}"
|
||||
)
|
||||
Vendored
+2
@@ -8,6 +8,8 @@ orcaslicer_add_cmake_project(NLopt
|
||||
-DNLOPT_GUILE:BOOL=OFF
|
||||
-DNLOPT_SWIG:BOOL=OFF
|
||||
-DNLOPT_TESTS:BOOL=OFF
|
||||
# testopt is built regardless of NLOPT_TESTS; see deps-windows.cmake.
|
||||
"${DEP_LLD_FORCE_MULTIPLE}"
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
|
||||
Vendored
+6
@@ -80,6 +80,12 @@ ExternalProject_Add(dep_OpenSSL
|
||||
INSTALL_COMMAND ${_install_cmd}
|
||||
)
|
||||
|
||||
if (CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
# OpenSSL builds with cl, but MSBuild runs nmake in this project's toolset
|
||||
# environment, and ClangCL's puts clang's headers first. Use the default.
|
||||
set_target_properties(dep_OpenSSL PROPERTIES VS_PLATFORM_TOOLSET "$(DefaultPlatformToolset)")
|
||||
endif ()
|
||||
|
||||
ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
|
||||
DEPENDEES install
|
||||
|
||||
|
||||
Vendored
+9
@@ -42,6 +42,15 @@ else ()
|
||||
message(FATAL_ERROR "Unsupported OS architecture: ${DEPS_ARCH}")
|
||||
endif ()
|
||||
|
||||
# Draco's tools and NLopt's testopt compile sources that are also in their
|
||||
# static library. MSBuild passes the library before the objects and lld-link
|
||||
# resolves as it goes, so the library's copy wins and the object then reads as
|
||||
# a duplicate. Nothing uses those executables, so let lld keep the first one.
|
||||
set(DEP_LLD_FORCE_MULTIPLE "")
|
||||
if (CMAKE_GENERATOR MATCHES "Visual Studio" AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
set(DEP_LLD_FORCE_MULTIPLE "-DCMAKE_EXE_LINKER_FLAGS:STRING=${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE")
|
||||
endif ()
|
||||
|
||||
if (${DEP_DEBUG})
|
||||
set(DEP_BOOST_DEBUG "debug")
|
||||
else ()
|
||||
|
||||
@@ -87,6 +87,7 @@ using namespace nlohmann;
|
||||
#include "dev-utils/BaseException.h"
|
||||
#endif
|
||||
#include "slic3r/Utils/MeshInspect.hpp"
|
||||
#include "slic3r/Utils/PaintCLI.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/GUI/BitmapCache.hpp"
|
||||
#include "slic3r/GUI/OpenGLManager.hpp"
|
||||
@@ -1443,6 +1444,29 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
// --inspect-paint prints its JSON and exits, so any action that does work of its
|
||||
// own (slicing, exporting) would be skipped without notice. Reject those up front;
|
||||
// only options that merely tune how the input is loaded may come along.
|
||||
if (std::find(m_actions.begin(), m_actions.end(), "inspect_paint") != m_actions.end()) {
|
||||
static const std::set<std::string> inspect_compatible = { "inspect_paint", "uptodate", "load_defaultfila", "min_save",
|
||||
"mtcpp", "mstpp", "no_check", "normative_check", "pipe" };
|
||||
for (const std::string &action : m_actions) {
|
||||
if (inspect_compatible.count(action) == 0) {
|
||||
std::string flag = action;
|
||||
std::replace(flag.begin(), flag.end(), '_', '-');
|
||||
boost::nowide::cerr << "--inspect-paint 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);
|
||||
}
|
||||
}
|
||||
// Without input there is nothing to inspect; fail rather than print nothing and exit 0.
|
||||
if (m_input_files.empty() && m_config.opt_string("load_assemble_list").empty()) {
|
||||
boost::nowide::cerr << "--inspect-paint needs an input file or --load-assemble-list" << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
|
||||
// --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.
|
||||
@@ -6100,6 +6124,30 @@ int CLI::run(int argc, char **argv)
|
||||
cli_status_callback(slicing_status);
|
||||
}
|
||||
g_cli_callback_mgr.stop();
|
||||
#endif
|
||||
for (Model &m : m_models)
|
||||
m.remove_backup_path_if_exist();
|
||||
record_exit_reson(outfile_dir, CLI_SUCCESS, plate_to_slice, cli_errors[CLI_SUCCESS], sliced_info);
|
||||
boost::nowide::cerr.flush();
|
||||
return CLI_SUCCESS;
|
||||
} else if (opt_key == "inspect_paint") {
|
||||
// --inspect-paint — read the per-facet enforcer/blocker/extruder/
|
||||
// fuzzy state from the loaded model and emit a JSON summary.
|
||||
// Machine-readable alternative to opening the paint gizmos.
|
||||
for (Model &model : m_models) {
|
||||
model.add_default_instances();
|
||||
Slic3r::PaintCLI::inspect_to_json(model, m_input_files, boost::nowide::cout);
|
||||
}
|
||||
boost::nowide::cout.flush();
|
||||
// The tooltip promises "then exit"; conflicting actions were rejected before
|
||||
// loading. Finish like the end of run(). flush_and_exit() is not usable here:
|
||||
// it prints "found error ..." to stdout, which would corrupt the JSON.
|
||||
#if defined(__linux__) || defined(__LINUX__)
|
||||
if (g_cli_callback_mgr.is_started()) {
|
||||
PrintBase::SlicingStatus slicing_status{100, "All done, Success"};
|
||||
cli_status_callback(slicing_status);
|
||||
}
|
||||
g_cli_callback_mgr.stop();
|
||||
#endif
|
||||
for (Model &m : m_models)
|
||||
m.remove_backup_path_if_exist();
|
||||
|
||||
@@ -2735,6 +2735,28 @@ void ToolOrdering::enforce_mixed_component_order()
|
||||
}
|
||||
}
|
||||
|
||||
// Declared in ToolOrdering.hpp (exposed for unit testing).
|
||||
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders)
|
||||
{
|
||||
std::vector<unsigned int> order;
|
||||
for (const std::string& token : split_string(str, ',')) {
|
||||
try {
|
||||
size_t pos = 0;
|
||||
int filament = std::stoi(token, &pos); // stoi skips leading whitespace by itself
|
||||
// stoi stops at the first non-digit, so "2x" would parse as 2. Require the whole token to be
|
||||
// consumed (bar trailing whitespace) to drop it like any other garbage.
|
||||
if (token.find_first_not_of(" \t\r\n", pos) != std::string::npos)
|
||||
continue;
|
||||
if (filament >= 1 && (unsigned int)filament <= number_of_extruders
|
||||
&& std::find(order.begin(), order.end(), (unsigned int)(filament - 1)) == order.end())
|
||||
order.emplace_back((unsigned int)(filament - 1));
|
||||
} catch (const std::exception&) {
|
||||
// Not a number, ignore it.
|
||||
}
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
|
||||
{
|
||||
const PrintConfig* print_config = m_print_config_ptr;
|
||||
@@ -2832,11 +2854,41 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
|
||||
const bool use_cyclic_ordering =
|
||||
(print_config->toolchange_ordering == ToolChangeOrderingType::Cyclic);
|
||||
|
||||
// By default the first layer keeps its adhesion-optimized order (and any custom first layer
|
||||
// sequence); the cyclic sequence is only forced onto it when the user opts in.
|
||||
const bool cyclic_first_layer = use_cyclic_ordering && print_config->toolchange_cyclic_first_layer.value;
|
||||
|
||||
// Optional user defined cyclic sequence, given as 1-based filament numbers ("3,2,1,4"). Filaments
|
||||
// missing from it keep their ascending order after the listed ones, so a partial or bogus entry
|
||||
// still yields the default cyclic order.
|
||||
const std::vector<unsigned int> cyclic_order =
|
||||
use_cyclic_ordering ? parse_cyclic_order(print_config->toolchange_cyclic_order.value, number_of_extruders)
|
||||
: std::vector<unsigned int>();
|
||||
|
||||
// Reorder a layer's filaments (0-based) for cyclic ordering: ascending by default, or following the
|
||||
// user defined sequence when one was given. Filaments absent from the sequence keep ascending order
|
||||
// after the listed ones.
|
||||
auto apply_cyclic_order = [&cyclic_order](std::vector<unsigned int>& filaments) {
|
||||
std::sort(filaments.begin(), filaments.end());
|
||||
if (!cyclic_order.empty())
|
||||
std::stable_sort(filaments.begin(), filaments.end(), [&cyclic_order](unsigned int lhs, unsigned int rhs) {
|
||||
auto rank = [&cyclic_order](unsigned int filament) {
|
||||
return size_t(std::find(cyclic_order.begin(), cyclic_order.end(), filament) - cyclic_order.begin());
|
||||
};
|
||||
return rank(lhs) < rank(rhs);
|
||||
});
|
||||
};
|
||||
|
||||
// other_layers_seq: the layer_idx and extruder_idx are base on 1
|
||||
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering](int layer_idx, std::vector<int>& out_seq) -> bool {
|
||||
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering, cyclic_first_layer, &apply_cyclic_order](int layer_idx, std::vector<int>& out_seq) -> bool {
|
||||
if (!reorder_first_layer && layer_idx == 0) {
|
||||
out_seq.resize(first_layer_filaments.size());
|
||||
std::transform(first_layer_filaments.begin(), first_layer_filaments.end(), out_seq.begin(), [](auto item) {return item + 1; });
|
||||
// The first layer tool order is already decided (adhesion-optimized, plus any custom first
|
||||
// layer sequence). Only override it with the cyclic sequence when the user opted in.
|
||||
std::vector<unsigned int> ordered = first_layer_filaments;
|
||||
if (cyclic_first_layer)
|
||||
apply_cyclic_order(ordered);
|
||||
out_seq.resize(ordered.size());
|
||||
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) {return int(item) + 1; });
|
||||
return true;
|
||||
}
|
||||
for (size_t idx = other_layers_seqs.size() - 1; idx != size_t(-1); --idx) {
|
||||
@@ -2847,9 +2899,12 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
|
||||
}
|
||||
}
|
||||
|
||||
if (use_cyclic_ordering && layer_idx >= 0 && size_t(layer_idx) < layer_filaments.size()) {
|
||||
// Skip the first layer here (layer_idx == 0 only reaches this point on the reorder_first_layer
|
||||
// path) unless the user asked for cyclic order on it, so it keeps the default flush ordering.
|
||||
if (use_cyclic_ordering && layer_idx >= 0 && (layer_idx != 0 || cyclic_first_layer)
|
||||
&& size_t(layer_idx) < layer_filaments.size()) {
|
||||
std::vector<unsigned int> ordered = layer_filaments[size_t(layer_idx)];
|
||||
std::sort(ordered.begin(), ordered.end());
|
||||
apply_cyclic_order(ordered);
|
||||
out_seq.resize(ordered.size());
|
||||
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) { return int(item) + 1; });
|
||||
return true;
|
||||
|
||||
@@ -417,6 +417,11 @@ private:
|
||||
int most_used_extruder;
|
||||
};
|
||||
|
||||
// Parse the user defined cyclic toolchange sequence ("3,2 , 1 , 4") into 0-based filament indices.
|
||||
// Out-of-range entries, duplicates and non-numeric tokens are dropped, so a partially valid string
|
||||
// still orders the filaments it does name. Exposed for unit testing.
|
||||
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders);
|
||||
|
||||
} // namespace SLic3r
|
||||
|
||||
#endif /* slic3r_ToolOrdering_hpp_ */
|
||||
|
||||
@@ -1320,6 +1320,8 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"wipe_tower_extra_flow",
|
||||
"single_extruder_multi_material_priming",
|
||||
"toolchange_ordering",
|
||||
"toolchange_cyclic_order",
|
||||
"toolchange_cyclic_first_layer",
|
||||
"wipe_tower_rotation_angle",
|
||||
"tree_support_branch_distance_organic",
|
||||
"tree_support_branch_diameter_organic",
|
||||
|
||||
@@ -360,6 +360,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
|| opt_key == "other_layers_print_sequence"
|
||||
|| opt_key == "other_layers_print_sequence_nums"
|
||||
|| opt_key == "toolchange_ordering"
|
||||
|| opt_key == "toolchange_cyclic_order"
|
||||
|| opt_key == "toolchange_cyclic_first_layer"
|
||||
|| opt_key == "extruder_ams_count"
|
||||
|| opt_key == "extruder_nozzle_stats"
|
||||
|| opt_key == "filament_map_mode"
|
||||
|
||||
@@ -6700,6 +6700,34 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_labels.emplace_back(L("Cyclic"));
|
||||
def->set_default_value(new ConfigOptionEnum<ToolChangeOrderingType>(ToolChangeOrderingType::Default));
|
||||
|
||||
def = this->add("toolchange_cyclic_order", coString);
|
||||
def->label = L("Cyclic order");
|
||||
def->category = L("Advanced");
|
||||
def->tooltip = L(
|
||||
"Custom filament sequence used by the cyclic toolchange ordering, as filament numbers separated by commas (e.g. \"3,2,1,4\").\n"
|
||||
"Each layer prints its filaments following this sequence; filaments not listed are printed last, in ascending order.\n"
|
||||
"Leave empty to cycle through the filaments in ascending order."
|
||||
);
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionString(""));
|
||||
|
||||
def = this->add("toolchange_cyclic_first_layer", coBool);
|
||||
def->label = L("Apply cyclic order to first layer");
|
||||
def->category = L("Advanced");
|
||||
def->tooltip = L(
|
||||
"Applies the cyclic toolchange order to the first layer as well.\n"
|
||||
"By default this is disabled, because the first layer is instead ordered for the best bed "
|
||||
"adhesion: filaments that print small, fragile first-layer features are printed last, so the "
|
||||
"following tool changes and travel moves are less likely to knock those weakly anchored parts "
|
||||
"loose. This first-layer order also honors a custom first layer filament sequence when one is set. "
|
||||
"The cyclic order's benefit (extra tool changes give each layer more time to cool) does not apply "
|
||||
"to the first layer, which is printed slowly and hot for adhesion.\n"
|
||||
"Enable this only if you need the exact same tool sequence on every layer, including the first, at "
|
||||
"the cost of that adhesion optimization."
|
||||
);
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("slice_closing_radius", coFloat);
|
||||
def->label = L("Slice gap closing radius");
|
||||
def->category = L("Quality");
|
||||
@@ -11963,6 +11991,19 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
||||
"the --ground-* options choose from. Machine-readable alternative to --info.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
// --inspect-paint \u2014 dump the per-facet enforcer/blocker/extruder/fuzzy
|
||||
// paint state stored on the loaded model (supports, seam, MMU color,
|
||||
// fuzzy-skin) as JSON. Read-only; lets CI / scripted / AI tooling
|
||||
// reason about existing paint on a .3mf without loading the GUI.
|
||||
def = this->add("inspect_paint", coBool);
|
||||
def->label = L("Inspect paint (JSON to stdout)");
|
||||
def->tooltip = L("Print a structured JSON summary of every painted layer "
|
||||
"(supports, seam, MMU color, fuzzy-skin) already stored on "
|
||||
"the loaded model \u2014 per-state facet count, surface area, "
|
||||
"and mesh-local bounding box \u2014 then exit. Machine-readable "
|
||||
"alternative to opening the paint gizmos in the GUI.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("export_settings", coString);
|
||||
def->label = L("Export Settings");
|
||||
def->tooltip = L("This exports settings to a file. Use - to write them to stdout.");
|
||||
|
||||
@@ -1627,6 +1627,8 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionBool, manual_filament_change))
|
||||
((ConfigOptionBool, single_extruder_multi_material_priming))
|
||||
((ConfigOptionEnum<ToolChangeOrderingType>, toolchange_ordering))
|
||||
((ConfigOptionString, toolchange_cyclic_order))
|
||||
((ConfigOptionBool, toolchange_cyclic_first_layer))
|
||||
((ConfigOptionBool, wipe_tower_no_sparse_layers))
|
||||
((ConfigOptionString, change_filament_gcode))
|
||||
((ConfigOptionString, change_extrusion_role_gcode))
|
||||
|
||||
@@ -682,6 +682,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/Bonjour.hpp
|
||||
Utils/MeshInspect.cpp
|
||||
Utils/MeshInspect.hpp
|
||||
Utils/PaintCLI.cpp
|
||||
Utils/PaintCLI.hpp
|
||||
Utils/CalibUtils.cpp
|
||||
Utils/CalibUtils.hpp
|
||||
Utils/ColorSpaceConvert.cpp
|
||||
|
||||
@@ -1055,6 +1055,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
|
||||
toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2);
|
||||
|
||||
bool use_cyclic_ordering = config->opt_enum<ToolChangeOrderingType>("toolchange_ordering") == ToolChangeOrderingType::Cyclic;
|
||||
toggle_line("toolchange_cyclic_order", use_cyclic_ordering);
|
||||
toggle_line("toolchange_cyclic_first_layer", use_cyclic_ordering);
|
||||
|
||||
toggle_line("prime_volume",have_prime_tower && (!purge_in_primetower || !bSEMM));
|
||||
|
||||
for (auto el : {"flush_into_infill", "flush_into_support", "flush_into_objects"})
|
||||
|
||||
@@ -3026,6 +3026,8 @@ void TabPrint::build()
|
||||
optgroup = page->new_optgroup(L("Advanced"), L"advanced");
|
||||
optgroup->append_single_option_line("interlocking_beam", "multimaterial_settings_advanced#interlocking-beam");
|
||||
optgroup->append_single_option_line("toolchange_ordering", "multimaterial_settings_advanced#toolchange-ordering");
|
||||
optgroup->append_single_option_line("toolchange_cyclic_order", "multimaterial_settings_advanced#toolchange-order");
|
||||
optgroup->append_single_option_line("toolchange_cyclic_first_layer", "multimaterial_settings_advanced#toolchange-order");
|
||||
optgroup->append_single_option_line("interface_shells", "multimaterial_settings_advanced#interface-shells");
|
||||
optgroup->append_single_option_line("mmu_segmented_region_max_width", "multimaterial_settings_advanced#maximum-width-of-segmented-region");
|
||||
optgroup->append_single_option_line("mmu_segmented_region_interlocking_depth", "multimaterial_settings_advanced#interlocking-depth-of-segmented-region");
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
// PaintCLI.cpp — CLI paint-inspection primitives. See PaintCLI.hpp.
|
||||
#include "PaintCLI.hpp"
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace PaintCLI {
|
||||
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
double its_surface_area(const indexed_triangle_set &its)
|
||||
{
|
||||
double total = 0.0;
|
||||
for (const stl_triangle_vertex_indices &t : its.indices) {
|
||||
const Vec3f &a = its.vertices[t(0)];
|
||||
const Vec3f &b = its.vertices[t(1)];
|
||||
const Vec3f &c = its.vertices[t(2)];
|
||||
total += 0.5 * (b - a).cross(c - a).norm();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// Bbox over triangle-referenced vertices only. get_facets_strict() returns
|
||||
// an itset with the full source vertex list — using bounding_box() on it
|
||||
// would report the whole mesh's bbox even when only a few facets are painted.
|
||||
BoundingBoxf3 its_referenced_bbox(const indexed_triangle_set &its)
|
||||
{
|
||||
BoundingBoxf3 bb;
|
||||
bool first = true;
|
||||
for (const stl_triangle_vertex_indices &t : its.indices) {
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const Vec3d v = its.vertices[t(k)].cast<double>();
|
||||
if (first) { bb.min = bb.max = v; first = false; }
|
||||
else bb.merge(v);
|
||||
}
|
||||
}
|
||||
return bb;
|
||||
}
|
||||
|
||||
json vec3_to_json(const Vec3d &v)
|
||||
{
|
||||
return json::array({ v.x(), v.y(), v.z() });
|
||||
}
|
||||
|
||||
json bbox_to_json(const BoundingBoxf3 &bb)
|
||||
{
|
||||
return {
|
||||
{ "min", vec3_to_json(bb.min) },
|
||||
{ "max", vec3_to_json(bb.max) },
|
||||
{ "size", vec3_to_json(Vec3d(bb.max - bb.min)) },
|
||||
};
|
||||
}
|
||||
|
||||
// One (layer, state) row — empty ones are omitted at the caller level.
|
||||
json state_entry(const std::string &label, const indexed_triangle_set &its)
|
||||
{
|
||||
return {
|
||||
{ "state", label },
|
||||
{ "facets", its.indices.size() },
|
||||
{ "area_mm2", its_surface_area(its) },
|
||||
{ "bbox", bbox_to_json(its_referenced_bbox(its)) },
|
||||
};
|
||||
}
|
||||
|
||||
// Iterate the states relevant to one FacetsAnnotation kind, collecting
|
||||
// non-empty entries. Empty layer → {"empty": true}. `n_facets_out` is the
|
||||
// running total of painted facets — bumped for the summary.
|
||||
json inspect_layer(const ModelVolume &mv, const FacetsAnnotation &fa,
|
||||
const std::vector<std::pair<EnforcerBlockerType, std::string>> &states,
|
||||
size_t &n_facets_out)
|
||||
{
|
||||
if (fa.empty())
|
||||
return { { "empty", true } };
|
||||
|
||||
json entries = json::array();
|
||||
for (const auto &st : states) {
|
||||
if (!fa.has_facets(mv, st.first))
|
||||
continue;
|
||||
indexed_triangle_set its = fa.get_facets_strict(mv, st.first);
|
||||
if (its.indices.empty())
|
||||
continue;
|
||||
n_facets_out += its.indices.size();
|
||||
entries.push_back(state_entry(st.second, its));
|
||||
}
|
||||
return {
|
||||
{ "empty", entries.empty() },
|
||||
{ "states", std::move(entries) },
|
||||
};
|
||||
}
|
||||
|
||||
const std::vector<std::pair<EnforcerBlockerType, std::string>> &supports_states()
|
||||
{
|
||||
static const std::vector<std::pair<EnforcerBlockerType, std::string>> s = {
|
||||
{ EnforcerBlockerType::ENFORCER, "ENFORCER" },
|
||||
{ EnforcerBlockerType::BLOCKER, "BLOCKER" },
|
||||
};
|
||||
return s;
|
||||
}
|
||||
|
||||
const std::vector<std::pair<EnforcerBlockerType, std::string>> &fuzzy_states()
|
||||
{
|
||||
// FUZZY_SKIN is an enum alias for ENFORCER; the layer is single-state.
|
||||
static const std::vector<std::pair<EnforcerBlockerType, std::string>> s = {
|
||||
{ EnforcerBlockerType::FUZZY_SKIN, "FUZZY_SKIN" },
|
||||
};
|
||||
return s;
|
||||
}
|
||||
|
||||
const std::vector<std::pair<EnforcerBlockerType, std::string>> &mmu_states()
|
||||
{
|
||||
static std::vector<std::pair<EnforcerBlockerType, std::string>> s = []{
|
||||
std::vector<std::pair<EnforcerBlockerType, std::string>> v;
|
||||
for (int i = 1; i <= int(EnforcerBlockerType::ExtruderMax); ++i)
|
||||
v.emplace_back(EnforcerBlockerType(i), "extruder_" + std::to_string(i));
|
||||
return v;
|
||||
}();
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void inspect_to_json(const Model &model, const std::vector<std::string> &source_paths,
|
||||
std::ostream &out)
|
||||
{
|
||||
json root;
|
||||
root["sources"] = source_paths;
|
||||
root["frame"] = "mesh_local";
|
||||
root["note"] = "Coordinates are mesh-local (each volume's own frame). "
|
||||
"Paint gizmos operate in this frame.";
|
||||
|
||||
json objects = json::array();
|
||||
size_t total_objects = 0, total_volumes = 0, total_painted = 0, total_facets = 0;
|
||||
|
||||
for (size_t oi = 0; oi < model.objects.size(); ++oi) {
|
||||
const ModelObject *mo = model.objects[oi];
|
||||
if (!mo) continue;
|
||||
++total_objects;
|
||||
|
||||
json obj;
|
||||
obj["index"] = oi;
|
||||
obj["name"] = mo->name;
|
||||
|
||||
json volumes = json::array();
|
||||
for (size_t vi = 0; vi < mo->volumes.size(); ++vi) {
|
||||
const ModelVolume *mv = mo->volumes[vi];
|
||||
if (!mv) continue;
|
||||
++total_volumes;
|
||||
|
||||
const indexed_triangle_set &its = mv->mesh().its;
|
||||
json vol;
|
||||
vol["index"] = vi;
|
||||
vol["name"] = mv->name;
|
||||
vol["n_facets"] = its.indices.size();
|
||||
vol["is_model_part"] = mv->is_model_part();
|
||||
vol["bbox_mesh_local"] = bbox_to_json(bounding_box(its));
|
||||
|
||||
size_t vol_painted = 0;
|
||||
json paints;
|
||||
paints["supports"] = inspect_layer(*mv, mv->supported_facets,
|
||||
supports_states(), vol_painted);
|
||||
paints["seam"] = inspect_layer(*mv, mv->seam_facets,
|
||||
supports_states(), vol_painted);
|
||||
paints["mmu_segmentation"] = inspect_layer(*mv, mv->mmu_segmentation_facets,
|
||||
mmu_states(), vol_painted);
|
||||
paints["fuzzy_skin"] = inspect_layer(*mv, mv->fuzzy_skin_facets,
|
||||
fuzzy_states(), vol_painted);
|
||||
vol["paints"] = std::move(paints);
|
||||
vol["painted_facets_total"] = vol_painted;
|
||||
|
||||
if (vol_painted > 0) ++total_painted;
|
||||
total_facets += vol_painted;
|
||||
|
||||
volumes.push_back(std::move(vol));
|
||||
}
|
||||
obj["volumes"] = std::move(volumes);
|
||||
objects.push_back(std::move(obj));
|
||||
}
|
||||
root["objects"] = std::move(objects);
|
||||
root["summary"] = {
|
||||
{ "objects", total_objects },
|
||||
{ "volumes", total_volumes },
|
||||
{ "volumes_with_paint", total_painted },
|
||||
{ "painted_facets_total", total_facets },
|
||||
};
|
||||
|
||||
// Object names and file paths are arbitrary bytes, and dump() throws on invalid
|
||||
// UTF-8 by default. Replace such sequences with U+FFFD so the output is always
|
||||
// valid JSON rather than an exception out of the CLI.
|
||||
out << root.dump(2, ' ', false, json::error_handler_t::replace) << std::endl;
|
||||
}
|
||||
|
||||
} // namespace PaintCLI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,31 @@
|
||||
// PaintCLI.hpp — CLI paint-inspection primitives.
|
||||
//
|
||||
// Backs the --inspect-paint CLI action. Reads the per-facet enforcer /
|
||||
// blocker / extruder / fuzzy-skin state that OrcaSlicer stores on every
|
||||
// ModelVolume (supports, seam, MMU color, fuzzy-skin) and emits a
|
||||
// structured JSON summary — facet count, surface area, and mesh-local
|
||||
// bounding box per state — so CI / scripted / AI tooling can reason
|
||||
// about existing paint on a .3mf without opening the GUI.
|
||||
//
|
||||
// Coordinates are mesh-local (each volume's own frame), matching the
|
||||
// frame that the paint gizmos operate in.
|
||||
#ifndef slic3r_PaintCLI_hpp_
|
||||
#define slic3r_PaintCLI_hpp_
|
||||
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
class Model;
|
||||
|
||||
namespace PaintCLI {
|
||||
|
||||
// `source_paths` lists every input file; the CLI merges them into one Model.
|
||||
void inspect_to_json(const Model &model, const std::vector<std::string> &source_paths,
|
||||
std::ostream &out);
|
||||
|
||||
} // namespace PaintCLI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
@@ -1025,3 +1025,41 @@ TEST_CASE("Selector slicing keeps the result valid across re-apply", "[Print][H2
|
||||
REQUIRE(status != PrintBase::APPLY_STATUS_INVALIDATED);
|
||||
REQUIRE(print.is_step_done(psSlicingFinished));
|
||||
}
|
||||
|
||||
TEST_CASE("parse_cyclic_order parses user cyclic toolchange sequences", "[ToolOrdering][Cyclic]")
|
||||
{
|
||||
// Filament numbers are 1-based in the UI; the parser returns 0-based indices.
|
||||
SECTION("well-formed sequence") {
|
||||
REQUIRE(parse_cyclic_order("3,2,1,4", 4) == std::vector<unsigned int>({2, 1, 0, 3}));
|
||||
}
|
||||
|
||||
SECTION("surrounding whitespace is tolerated") {
|
||||
REQUIRE(parse_cyclic_order(" 3 , 2 ,1, 4 ", 4) == std::vector<unsigned int>({2, 1, 0, 3}));
|
||||
}
|
||||
|
||||
SECTION("out-of-range and non-positive entries are dropped") {
|
||||
// 0 is below the 1-based range, 5 is above it for a 4-filament setup, -1 is invalid.
|
||||
REQUIRE(parse_cyclic_order("0,5,-1,2", 4) == std::vector<unsigned int>({1}));
|
||||
}
|
||||
|
||||
SECTION("duplicates keep only the first occurrence") {
|
||||
REQUIRE(parse_cyclic_order("2,2,1,2", 4) == std::vector<unsigned int>({1, 0}));
|
||||
}
|
||||
|
||||
SECTION("garbage tokens are ignored") {
|
||||
REQUIRE(parse_cyclic_order("3,abc,,2,x1", 4) == std::vector<unsigned int>({2, 1}));
|
||||
}
|
||||
|
||||
SECTION("tokens that only start with a number are ignored") {
|
||||
// "2x" must be dropped rather than parsed as filament 2.
|
||||
REQUIRE(parse_cyclic_order("3,2x,1", 4) == std::vector<unsigned int>({2, 0}));
|
||||
}
|
||||
|
||||
SECTION("empty string yields an empty order") {
|
||||
REQUIRE(parse_cyclic_order("", 4).empty());
|
||||
}
|
||||
|
||||
SECTION("a partial sequence only names the filaments it lists") {
|
||||
REQUIRE(parse_cyclic_order("3,1", 4) == std::vector<unsigned int>({2, 0}));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user