Compare commits

...
Author SHA1 Message Date
Valerii Bokhan 60b4a61854 Fix: Show indexed coFloatsOrPercents options in unsaved changes dialog (#15472) 2026-09-17 10:56:17 -03:00
Ian Bassi 7065fa9eae Fix extruder clearance help link anchor (#15738) 2026-09-17 09:54:42 -03:00
Ian Bassi 59e40a2c2e Print unsupported walls last (#15411) 2026-09-17 09:14:20 -03:00
Ian Bassi 82e91bd472 Port wipe tower BBS improvements (#15485) 2026-09-17 09:08:50 -03:00
packerlschupfer ca668a3bc9 CLI: --inspect-paint — dump per-facet paint state as JSON (#14608)
* CLI: --inspect-paint — dump per-facet paint state as JSON

Reads the per-facet enforcer/blocker/extruder/fuzzy-skin state stored
on every ModelVolume (supported_facets / seam_facets /
mmu_segmentation_facets / fuzzy_skin_facets) and emits a structured
JSON summary to stdout. Machine-readable alternative to opening the
paint gizmos.

Per (object, volume, layer, state): facet count, surface area in
mm², and mesh-local bounding box. Empty layers collapse to
{"empty": true}. Summary at the top level rolls up totals.

One correctness detail worth calling out: FacetsAnnotation::
get_facets_strict returns an indexed_triangle_set whose `vertices`
array is the whole source mesh — only `indices` are filtered to the
painted triangles. A naive bounding_box(its) would report the whole
mesh's bbox even when only a few facets are painted. The helper
its_referenced_bbox() walks only the vertices actually indexed by
the painted triangles, so `bbox` correctly localizes the painted
region.

Rationale: every paint-driven workflow — GUI-painted .3mf verified
in CI, AI agents planning support enforcers, MMU color layout checks
— needs to know what's already painted on a model. Today that's a
GUI-only read. --inspect-paint closes that loop for scripted callers.

New file src/slic3r/Utils/PaintCLI.{hpp,cpp} (~215 lines). Depends
only on Model, TriangleMesh, TriangleSelector, FacetsAnnotation, and
nlohmann::json — all already in tree. No new dependencies, no
signature changes, no behavior change when the flag is absent.

Registered as an action (parallel to --info) so it satisfies the
"needs an action" check and bypasses the GUI fallback; control falls
through the normal post-action path to a clean exit 0.

Verification:
  unpainted STL:     every layer {"empty": true}, summary zero
  GUI-painted .3mf:  enforcer count / area / bbox match painter
  clean JSON:        parseable via jq

* CLI --inspect-paint: exit after printing, reject conflicting actions

- Finish like the end of CLI::run once the JSON is written, as the
  tooltip says. The callback manager is Linux-only, so its use is
  guarded.
- Reject actions that would otherwise be skipped without notice
  (--slice, --export-3mf, ...) before loading. Load-time options such as
  --uptodate are still accepted.
- Replace invalid UTF-8 in object names and paths instead of throwing.
- Report every input file as sources; inputs are merged into one model
  before actions run.

* CLI --inspect-paint: reject a run without input

Without an input file or --load-assemble-list there is nothing to
inspect, and the run printed nothing and exited 0. Reject it up front
with CLI_INVALID_PARAMS, next to the other invalid-parameter checks.
2026-09-17 12:01:49 +08:00
Kris Austin 6b0e190e64 ci: key the Windows compiler cache on the MSVC toolset version (#15729) 2026-09-16 18:30:18 -03:00
Kris Austin 8effa27f4a build: build the dependencies with clang-cl under the Visual Studio generator (#15673)
The deps superbuild passes the Visual Studio generator and platform to
every sub-build but not the toolset, so build_win.bat -d -l without -x
compiled every dependency with cl even though the superbuild had been
configured with -T ClangCL; CMake replaces the forwarded
CMAKE_<LANG>_COMPILER with whatever the toolset ran. The recipes that
adapt to clang-cl then disagreed with what had been built, and
wxInspector told FindwxWidgets to look in lib/clang_x64_lib while the
cl-built wxWidgets had installed into lib/vc_x64_lib:

  Could NOT find wxWidgets (missing: wxWidgets_LIBRARIES
  wxWidgets_INCLUDE_DIRS core base aui propgrid)

Forward CMAKE_GENERATOR_TOOLSET as well, so the dependencies compile
with clang-cl under MSBuild the way they already do under Ninja. Four
of them need more than that:

- OpenSSL always builds with cl, and MSBuild runs its nmake steps in
  the project's toolset environment, where ClangCL puts clang's include
  directory first and cl trips over clang's stdint.h. The project gets
  the default toolset.
- Boost.Container's dlmalloc needs -Wno-incompatible-pointer-types
  under clang. boost_container links as C++, and the Visual Studio
  generator writes only the link language's flags into the project, so
  its C file never saw CMAKE_C_FLAGS. Under that generator the option
  goes through the C++ flags as well, with the defaults kept.
- Draco's tools and NLopt's testopt compile sources their own static
  library also contains. MSBuild lists libraries before objects and
  lld-link resolves archive members as each input arrives, so the
  library's copy is pulled in before the executable's own object and
  the link fails on duplicate symbols; link.exe defers the search and
  Ninja lists the objects first. Nothing uses those executables, so
  they get /FORCE:MULTIPLE there.

The Ninja path is unchanged: the generated configure commands of all
29 dependencies are identical before and after. OCCT's arm64 override
to cl still applies under Ninja but not under the Visual Studio
generator, where the toolset wins; that combination never built and is
left for a follow-up.
2026-09-16 14:28:50 -03:00
Ian BassiandRodrigo Faselli 72774e5398 Toolchange Cyclic Order (#14868)
* Toolchange Cyclic Order

* Apply cyclic order to first layer

* Unit test

* Copilot fixes

---------

Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-09-16 12:19:03 -03:00
40 changed files with 1881 additions and 82 deletions
+9
View File
@@ -85,6 +85,15 @@ jobs:
shell: bash shell: bash
run: | run: |
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}" 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_LEG=$leg" >> "$GITHUB_ENV"
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV" echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
+8
View File
@@ -27,8 +27,15 @@ endif ()
# Boost.Container's bundled dlmalloc passes int* where the Win32 Interlocked API # Boost.Container's bundled dlmalloc passes int* where the Win32 Interlocked API
# takes volatile long*; cl compiles that with a warning, clang errors out. # takes volatile long*; cl compiles that with a warning, clang errors out.
set(_boost_c_flags_line "") set(_boost_c_flags_line "")
set(_boost_cxx_flags_line "")
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang") if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
set(_boost_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-incompatible-pointer-types") 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 () endif ()
orcaslicer_add_cmake_project(Boost orcaslicer_add_cmake_project(Boost
@@ -46,6 +53,7 @@ orcaslicer_add_cmake_project(Boost
"${_context_arch_line}" "${_context_arch_line}"
"${_context_impl_line}" "${_context_impl_line}"
"${_boost_c_flags_line}" "${_boost_c_flags_line}"
"${_boost_cxx_flags_line}"
) )
set(DEP_Boost_DEPENDS ZLIB) set(DEP_Boost_DEPENDS ZLIB)
+5
View File
@@ -184,6 +184,11 @@ function(orcaslicer_add_cmake_project projectname)
if (_dep_msvc_gen) if (_dep_msvc_gen)
set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}") 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() else()
set(_gen "") set(_gen "")
endif() endif()
+3
View File
@@ -7,4 +7,7 @@ orcaslicer_add_cmake_project(Draco
${_options} ${_options}
URL https://github.com/google/draco/archive/refs/tags/1.5.7.zip URL https://github.com/google/draco/archive/refs/tags/1.5.7.zip
URL_HASH SHA256=27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77 URL_HASH SHA256=27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77
CMAKE_ARGS
# The encoder and decoder tools duplicate draco.lib; see deps-windows.cmake.
"${DEP_LLD_FORCE_MULTIPLE}"
) )
+2
View File
@@ -8,6 +8,8 @@ orcaslicer_add_cmake_project(NLopt
-DNLOPT_GUILE:BOOL=OFF -DNLOPT_GUILE:BOOL=OFF
-DNLOPT_SWIG:BOOL=OFF -DNLOPT_SWIG:BOOL=OFF
-DNLOPT_TESTS:BOOL=OFF -DNLOPT_TESTS:BOOL=OFF
# testopt is built regardless of NLOPT_TESTS; see deps-windows.cmake.
"${DEP_LLD_FORCE_MULTIPLE}"
) )
if (MSVC) if (MSVC)
+6
View File
@@ -80,6 +80,12 @@ ExternalProject_Add(dep_OpenSSL
INSTALL_COMMAND ${_install_cmd} 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 ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
DEPENDEES install DEPENDEES install
+9
View File
@@ -42,6 +42,15 @@ else ()
message(FATAL_ERROR "Unsupported OS architecture: ${DEPS_ARCH}") message(FATAL_ERROR "Unsupported OS architecture: ${DEPS_ARCH}")
endif () 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}) if (${DEP_DEBUG})
set(DEP_BOOST_DEBUG "debug") set(DEP_BOOST_DEBUG "debug")
else () else ()
+48
View File
@@ -87,6 +87,7 @@ using namespace nlohmann;
#include "dev-utils/BaseException.h" #include "dev-utils/BaseException.h"
#endif #endif
#include "slic3r/Utils/MeshInspect.hpp" #include "slic3r/Utils/MeshInspect.hpp"
#include "slic3r/Utils/PaintCLI.hpp"
#include "slic3r/GUI/PartPlate.hpp" #include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/BitmapCache.hpp" #include "slic3r/GUI/BitmapCache.hpp"
#include "slic3r/GUI/OpenGLManager.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 // --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 // too (--info, --help, --orient, slicing and exporting). The allowed ones do nothing when nothing is
// sliced or exported. // sliced or exported.
@@ -6100,6 +6124,30 @@ int CLI::run(int argc, char **argv)
cli_status_callback(slicing_status); cli_status_callback(slicing_status);
} }
g_cli_callback_mgr.stop(); 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 #endif
for (Model &m : m_models) for (Model &m : m_models)
m.remove_backup_path_if_exist(); m.remove_backup_path_if_exist();
+4
View File
@@ -454,6 +454,10 @@ class ExtrusionLoop : public ExtrusionEntity
{ {
public: public:
ExtrusionPaths paths; ExtrusionPaths paths;
// ORCA: Set on a loop extruded entirely in mid air and out of reach of the layer below: it has
// nothing to lean on until this layer is bridged, so the G-code writer holds it back until the
// infill is down. See defer_unsupported_loops() in PerimeterGenerator.cpp.
bool print_after_infill = false;
ExtrusionLoop(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {} ExtrusionLoop(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {}
ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) {} ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) {}
+62 -19
View File
@@ -1028,11 +1028,21 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
double current_z = gcodegen.writer().get_position().z(); double current_z = gcodegen.writer().get_position().z();
if (z == -1.) // in case no specific z was provided, print at current_z pos if (z == -1.) // in case no specific z was provided, print at current_z pos
z = current_z; z = current_z;
if (!is_approx(z, current_z)) { // Orca: wipe_tower_no_sparse_layers crash guard. With sparse layers skipped the tower is
// compacted far below the object, so descending to it is only safe once the nozzle is parked
// over the tower - which is what the is_finish_first travel above does. Otherwise the nozzle
// is still over the model and this descent would drive it into the print, so defer it to the
// re-descents below, which run after the travel to the tower.
const bool defer_compacted_descend = m_sparse_layers_skipped
&& !tcr.priming && !tcr.is_finish_first && (current_z - z) > EPSILON;
if (!is_approx(z, current_z) && !defer_compacted_descend) {
gcode += gcodegen.writer().retract(); gcode += gcodegen.writer().retract();
gcode += gcodegen.writer().travel_to_z(z, "Travel down to the last wipe tower layer."); gcode += gcodegen.writer().travel_to_z(z, "Travel down to the last wipe tower layer.");
gcode += gcodegen.writer().unretract(); gcode += gcodegen.writer().unretract();
} }
// Tower compacted below the object, so any extrusion emitted without an explicit z has to be
// pulled back down to it first.
const bool compacted_below_object = m_sparse_layers_skipped && z >= 0. && (tcr.print_z - z) > EPSILON;
// Process the end filament gcode. // Process the end filament gcode.
bool add_change_filament_624 = false; bool add_change_filament_624 = false;
@@ -1085,11 +1095,23 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
std::string nozzle_change_gcode_trans; std::string nozzle_change_gcode_trans;
if (is_nozzle_change) { if (is_nozzle_change) {
// move to start_pos before nozzle change // move to start_pos before nozzle change
// Orca: travel_to() lifts to the object layer height to clear the print. That lift is
// needed when arriving from the model, but is a wasted full-height Z bounce when the
// nozzle already sits on the compacted tower, so travel at the compacted z instead.
const bool compact_intower_nc_travel = compacted_below_object
&& (tcr.print_z - gcodegen.writer().get_position().z()) > EPSILON;
std::string start_pos_str; std::string start_pos_str;
start_pos_str = gcodegen.travel_to(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(tcr.nozzle_change_result.start_pos) + plate_origin_2d), erMixed, start_pos_str = gcodegen.travel_to(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(tcr.nozzle_change_result.start_pos) + plate_origin_2d), erMixed,
"Move to nozzle change start pos"); "Move to nozzle change start pos", compact_intower_nc_travel ? z : DBL_MAX);
check_add_eol(start_pos_str); check_add_eol(start_pos_str);
nozzle_change_gcode_trans += start_pos_str; nozzle_change_gcode_trans += start_pos_str;
// The nozzle-change wipe below carries no explicit z, so it would extrude at the object
// layer height and float above the compacted tower. Descend unless the travel stayed down.
if (!compact_intower_nc_travel && compacted_below_object) {
std::string nc_z_descend = gcodegen.writer().travel_to_z(z, "Descend to compacted wipe tower z (no sparse layers)");
check_add_eol(nc_z_descend);
nozzle_change_gcode_trans += nc_z_descend;
}
nozzle_change_gcode_trans += gcodegen.unretract(); nozzle_change_gcode_trans += gcodegen.unretract();
nozzle_change_gcode_trans += transform_gcode(tcr.nozzle_change_result.gcode, tcr.nozzle_change_result.start_pos, wipe_tower_offset, wipe_tower_rotation); nozzle_change_gcode_trans += transform_gcode(tcr.nozzle_change_result.gcode, tcr.nozzle_change_result.start_pos, wipe_tower_offset, wipe_tower_rotation);
gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(tcr.nozzle_change_result.end_pos) + plate_origin_2d)); gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(tcr.nozzle_change_result.end_pos) + plate_origin_2d));
@@ -1428,6 +1450,15 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
start_filament_gcode_str = start_filament_gcode_str + wipe_next_start_point_str + toolchange_unretract_str; start_filament_gcode_str = start_filament_gcode_str + wipe_next_start_point_str + toolchange_unretract_str;
// Orca: the custom change_filament_gcode lifts to the object layer height and the unretract
// de-hops back to it, so every tower extrusion emitted after it (purge moves, and the wall
// when it prints after the toolchange) would float above the compacted tower. Descend first.
if (compacted_below_object) {
std::string z_descend = gcodegen.writer().travel_to_z(z, "Descend to compacted wipe tower z (no sparse layers)");
check_add_eol(z_descend);
start_filament_gcode_str += z_descend;
}
// Insert the end filament, toolchange, and start filament gcode into the generated gcode. // Insert the end filament, toolchange, and start filament gcode into the generated gcode.
DynamicConfig config; DynamicConfig config;
config.set_key_value("filament_end_gcode", new ConfigOptionString(end_filament_gcode_str)); config.set_key_value("filament_end_gcode", new ConfigOptionString(end_filament_gcode_str));
@@ -1915,11 +1946,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
// resulting in a wipe tower with sparse layers. // resulting in a wipe tower with sparse layers.
double wipe_tower_z = -1; double wipe_tower_z = -1;
bool ignore_sparse = false; bool ignore_sparse = false;
if (gcodegen.config().wipe_tower_no_sparse_layers.value) { if (m_sparse_layers_skipped) {
wipe_tower_z = m_last_wipe_tower_print_z; wipe_tower_z = m_last_wipe_tower_print_z;
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 && ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]) && m_layer_idx != 0;
m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool &&
m_layer_idx != 0);
if (m_tool_change_idx == 0 && !ignore_sparse) if (m_tool_change_idx == 0 && !ignore_sparse)
wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height; wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height;
} }
@@ -1935,12 +1964,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
// resulting in a wipe tower with sparse layers. // resulting in a wipe tower with sparse layers.
double wipe_tower_z = -1; double wipe_tower_z = -1;
bool ignore_sparse = false; bool ignore_sparse = false;
if (gcodegen.config().wipe_tower_no_sparse_layers.value) { if (m_sparse_layers_skipped) {
wipe_tower_z = m_last_wipe_tower_print_z; ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 && wipe_tower_z = m_compacted_tower_z[m_layer_idx];
m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool);
if (m_tool_change_idx == 0 && !ignore_sparse)
wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height;
} }
if ((m_enable_timelapse_print || m_enable_wrapping_detection) && m_is_first_print) { if ((m_enable_timelapse_print || m_enable_wrapping_detection) && m_is_first_print) {
@@ -1953,10 +1979,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size())) if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size()))
throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer."); throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer.");
if (!ignore_sparse) { if (!ignore_sparse)
gcode += append_tcr(gcodegen, m_tool_changes[m_layer_idx][m_tool_change_idx++], extruder_id, wipe_tower_z); gcode += append_tcr(gcodegen, m_tool_changes[m_layer_idx][m_tool_change_idx++], extruder_id, wipe_tower_z);
m_last_wipe_tower_print_z = wipe_tower_z;
}
} }
} }
@@ -1970,9 +1994,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
return true; return true;
bool ignore_sparse = false; bool ignore_sparse = false;
if (gcodegen.config().wipe_tower_no_sparse_layers.value) { if (m_sparse_layers_skipped)
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 && m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool); ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
}
if ((m_enable_timelapse_print || m_enable_wrapping_detection) && m_is_first_print) { if ((m_enable_timelapse_print || m_enable_wrapping_detection) && m_is_first_print) {
return false; return false;
@@ -6580,6 +6603,8 @@ LayerResult GCode::process_layer(
} }
// Then print infill // Then print infill
gcode += this->extrude_infill(print, by_region_specific, false); gcode += this->extrude_infill(print, by_region_specific, false);
// Then the walls left hanging in mid air, now that the infill can anchor them
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false, true);
// Then print perimeters of regions that has is_infill_first == true // Then print perimeters of regions that has is_infill_first == true
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true); gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
} }
@@ -6875,6 +6900,7 @@ LayerResult GCode::process_layer(
has_insert_timelapse_gcode = true; has_insert_timelapse_gcode = true;
} }
gcode += this->extrude_infill(print, by_region_specific, false); gcode += this->extrude_infill(print, by_region_specific, false);
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false, true);
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true); gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
// ironing // ironing
gcode += this->extrude_infill(print, by_region_specific, true); gcode += this->extrude_infill(print, by_region_specific, true);
@@ -7615,7 +7641,7 @@ std::string GCode::extrude_path(const ExtrusionPath& path, const std::string& de
} }
// Extrude perimeters: Decide where to put seams (hide or align seams). // Extrude perimeters: Decide where to put seams (hide or align seams).
std::string GCode::extrude_perimeters(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, bool is_first_layer, bool is_infill_first) std::string GCode::extrude_perimeters(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, bool is_first_layer, bool is_infill_first, bool unsupported_loops_only)
{ {
std::string gcode; std::string gcode;
for (const ObjectByExtruder::Island::Region &region : by_region) for (const ObjectByExtruder::Island::Region &region : by_region)
@@ -7634,7 +7660,24 @@ std::string GCode::extrude_perimeters(const Print &print, const std::vector<Obje
m_config.wipe_inward_distance.value > 0. && m_config.wipe_inward_distance.value > 0. &&
scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON) scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON)
wipe_support.emplace(); wipe_support.emplace();
// ORCA: loops flagged as extruded in mid air, out of reach of the layer below, are held back
// for a second pass after the infill that anchors them. Infill already precedes infill first walls.
const bool defer_unsupported = !is_infill_first;
auto waits_for_infill = [](const ExtrusionEntity *ee) {
return ee->is_loop() && static_cast<const ExtrusionLoop *>(ee)->print_after_infill;
};
// The deferred pass runs after the infill, so the loops the first pass emitted are
// already down and belong in the prefix an inward wipe may land on.
if (wipe_support && defer_unsupported && unsupported_loops_only)
for (const ExtrusionEntity* ee : region.perimeters)
if (!waits_for_infill(ee))
wipe_support->append(*ee);
for (const ExtrusionEntity* ee : region.perimeters) { for (const ExtrusionEntity* ee : region.perimeters) {
if (defer_unsupported && waits_for_infill(ee) != unsupported_loops_only)
continue;
gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters, gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters,
wipe_support ? &*wipe_support : nullptr); wipe_support ? &*wipe_support : nullptr);
if (wipe_support) if (wipe_support)
+12 -2
View File
@@ -106,8 +106,13 @@ public:
m_enable_wrapping_detection(print_config.enable_wrapping_detection && (print_config.wrapping_exclude_area.values.size() > 2) && (slice_used_filaments.size() <= 1)), m_enable_wrapping_detection(print_config.enable_wrapping_detection && (print_config.wrapping_exclude_area.values.size() > 2) && (slice_used_filaments.size() <= 1)),
m_is_first_print(true), m_is_first_print(true),
m_print_config(&print_config), m_print_config(&print_config),
m_last_wipe_tower_print_z(print_config.z_offset.value) m_last_wipe_tower_print_z(print_config.z_offset.value),
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(print_config))
{ {
// Precomputed rather than accumulated while emitting, so that the clearance validator and
// the emitter cannot disagree about where the compacted tower sits on any given layer.
if (m_sparse_layers_skipped)
m_compacted_tower_z = compute_compacted_wipe_tower_z(tool_changes, float(print_config.z_offset.value));
// initialize with the extruder offset of master extruder id // initialize with the extruder offset of master extruder id
m_extruder_offsets.resize(print_config.filament_map.size(), print_config.extruder_offset.get_at(print_config.master_extruder_id.value - 1)); m_extruder_offsets.resize(print_config.filament_map.size(), print_config.extruder_offset.get_at(print_config.master_extruder_id.value - 1));
const auto& filament_map = print_config.filament_map.values; // 1 based idx const auto& filament_map = print_config.filament_map.values; // 1 based idx
@@ -167,6 +172,11 @@ private:
float m_wipe_tower_depth; float m_wipe_tower_depth;
BoundingBoxf m_wipe_tower_bbx; BoundingBoxf m_wipe_tower_bbx;
Vec2f m_rib_offset{Vec2f(0, 0)}; Vec2f m_rib_offset{Vec2f(0, 0)};
// wipe_tower_no_sparse_layers, as answered by the shared compaction rule rather than by the raw
// option: smooth timelapse and wrapping detection keep a tower on every layer regardless.
const bool m_sparse_layers_skipped;
// Print z of the compacted tower per planned layer. Empty when the tower is not compacted.
std::vector<float> m_compacted_tower_z;
}; };
class ColorPrintColors class ColorPrintColors
@@ -524,7 +534,7 @@ private:
// For sequential print, the instance of the object to be printing has to be defined. // For sequential print, the instance of the object to be printing has to be defined.
const size_t single_object_instance_idx); const size_t single_object_instance_idx);
std::string extrude_perimeters(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool is_first_layer, bool is_infill_first); std::string extrude_perimeters(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool is_first_layer, bool is_infill_first, bool unsupported_loops_only = false);
std::string extrude_infill(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool ironing); std::string extrude_infill(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool ironing);
std::string extrude_support(const ExtrusionEntityCollection& support_fills, const ExtrusionRole support_extrusion_role); std::string extrude_support(const ExtrusionEntityCollection& support_fills, const ExtrusionRole support_extrusion_role);
+60 -5
View File
@@ -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) void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
{ {
const PrintConfig* print_config = m_print_config_ptr; 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 = const bool use_cyclic_ordering =
(print_config->toolchange_ordering == ToolChangeOrderingType::Cyclic); (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 // 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) { if (!reorder_first_layer && layer_idx == 0) {
out_seq.resize(first_layer_filaments.size()); // The first layer tool order is already decided (adhesion-optimized, plus any custom first
std::transform(first_layer_filaments.begin(), first_layer_filaments.end(), out_seq.begin(), [](auto item) {return item + 1; }); // 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; return true;
} }
for (size_t idx = other_layers_seqs.size() - 1; idx != size_t(-1); --idx) { 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::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()); out_seq.resize(ordered.size());
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) { return int(item) + 1; }); std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) { return int(item) + 1; });
return true; return true;
+5
View File
@@ -417,6 +417,11 @@ private:
int most_used_extruder; 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 } // namespace SLic3r
#endif /* slic3r_ToolOrdering_hpp_ */ #endif /* slic3r_ToolOrdering_hpp_ */
+31 -7
View File
@@ -25,6 +25,30 @@ static constexpr int arc_fit_size = 20;
enum class LimitFlow { None, LimitPrintFlow, LimitRammingFlow, LimitRammingFlowNC};//nc:nozzle change enum class LimitFlow { None, LimitPrintFlow, LimitRammingFlow, LimitRammingFlowNC};//nc:nozzle change
static const std::map<float, float> nozzle_diameter_to_nozzle_change_width{{0.2f, 0.5f}, {0.4f, 1.0f}, {0.6f, 1.2f}, {0.8f, 1.4f}}; static const std::map<float, float> nozzle_diameter_to_nozzle_change_width{{0.2f, 0.5f}, {0.4f, 1.0f}, {0.6f, 1.2f}, {0.8f, 1.4f}};
bool wipe_tower_sparse_layers_skipped(const PrintConfig &config)
{
return config.wipe_tower_no_sparse_layers.value && config.timelapse_type.value != TimelapseType::tlSmooth &&
! config.enable_wrapping_detection.value;
}
bool wipe_tower_layer_is_sparse(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes)
{
return layer_tool_changes.size() == 1 && layer_tool_changes.front().initial_tool == layer_tool_changes.front().new_tool;
}
std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes,
float base_z)
{
std::vector<float> tower_z(tool_changes.size(), base_z);
float last = base_z;
for (size_t i = 0; i < tool_changes.size(); ++i) {
if (! tool_changes[i].empty() && ! wipe_tower_layer_is_sparse(tool_changes[i]))
last += tool_changes[i].front().layer_height;
tower_z[i] = last;
}
return tower_z;
}
inline float align_round(float value, float base) inline float align_round(float value, float base)
{ {
return std::round(value / base) * base; return std::round(value / base) * base;
@@ -1879,7 +1903,7 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi
m_z_pos(0.f), m_z_pos(0.f),
//m_bridging(float(config.wipe_tower_bridging)), //m_bridging(float(config.wipe_tower_bridging)),
m_bridging(10.f), m_bridging(10.f),
m_no_sparse_layers(config.wipe_tower_no_sparse_layers), m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
m_gcode_flavor(config.gcode_flavor), m_gcode_flavor(config.gcode_flavor),
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))), m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
m_current_tool(initial_tool), m_current_tool(initial_tool),
@@ -2977,7 +3001,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer(bool extrude_perimeter, bool
// Ask our writer about how much material was consumed. // Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (! m_no_sparse_layers || toolchanges_on_layer) if (! m_sparse_layers_skipped || toolchanges_on_layer)
if (m_current_tool < m_used_filament_length.size()) if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
@@ -3021,7 +3045,7 @@ void WipeTower::plan_toolchange(float z_par, float layer_height_par, unsigned in
if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) // if we moved to a new layer, we'll add it to m_plan first if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) // if we moved to a new layer, we'll add it to m_plan first
m_plan.push_back(WipeTowerInfo(z_par, layer_height_par)); m_plan.push_back(WipeTowerInfo(z_par, layer_height_par));
if (m_first_layer_idx == size_t(-1) && (! m_no_sparse_layers || old_tool != new_tool)) if (m_first_layer_idx == size_t(-1) && (! m_sparse_layers_skipped || old_tool != new_tool))
m_first_layer_idx = m_plan.size() - 1; m_first_layer_idx = m_plan.size() - 1;
if (old_tool == new_tool) // new layer without toolchanges - we are done if (old_tool == new_tool) // new layer without toolchanges - we are done
@@ -3874,7 +3898,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer_new(bool extrude_perimeter,
// Ask our writer about how much material was consumed. // Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_no_sparse_layers || toolchanges_on_layer) if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (m_current_tool < m_used_filament_length.size()) if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
@@ -3984,7 +4008,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block(const WipeTowerBlock &block,
// Ask our writer about how much material was consumed. // Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_no_sparse_layers || toolchanges_on_layer) if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (filament_id < m_used_filament_length.size()) if (filament_id < m_used_filament_length.size())
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length(); m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
@@ -4101,7 +4125,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block_solid(const WipeTowerBlock &
// Ask our writer about how much material was consumed. // Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_no_sparse_layers || toolchanges_on_layer) if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (filament_id < m_used_filament_length.size()) if (filament_id < m_used_filament_length.size())
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length(); m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
@@ -5155,7 +5179,7 @@ WipeTower::ToolChangeResult WipeTower::only_generate_out_wall(bool is_new_mode)
// Ask our writer about how much material was consumed. // Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_no_sparse_layers || toolchanges_on_layer) if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
return construct_tcr(writer, false, old_tool, true, false, 0.f, false); return construct_tcr(writer, false, old_tool, true, false, 0.f, false);
+19 -1
View File
@@ -521,7 +521,7 @@ private:
//float m_parking_pos_retraction = 0.f; //float m_parking_pos_retraction = 0.f;
//float m_extra_loading_move = 0.f; //float m_extra_loading_move = 0.f;
float m_bridging = 0.f; float m_bridging = 0.f;
bool m_no_sparse_layers = false; bool m_sparse_layers_skipped = false;
// BBS: remove useless config // BBS: remove useless config
//bool m_set_extruder_trimpot = false; //bool m_set_extruder_trimpot = false;
bool m_adhesion = true; bool m_adhesion = true;
@@ -680,6 +680,24 @@ private:
}; };
// Compaction rule for wipe_tower_no_sparse_layers. Shared by the G-code emitter and by the
// clearance validator so that both agree on where the compacted tower actually sits; a drift
// between the two would either let a real nozzle collision through or reject a safe plate.
// Whether sparse layers are really skipped, i.e. whether the tower is compacted at all. Smooth
// timelapse and wrapping detection put a tower on every layer, so no layer is ever dropped and the
// tower keeps following the object even though the option is on. Tower planning, G-code emission and
// the clearance validator all ask this single question, so none of them can compact on its own.
bool wipe_tower_sparse_layers_skipped(const PrintConfig &config);
// A planned layer prints no tower at all when its only toolchange keeps the same filament.
bool wipe_tower_layer_is_sparse(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes);
// Print z the compacted tower reaches on every planned layer. Sparse layers carry over the
// previous value, so the tower falls one layer height behind the object for each of them. base_z is
// the z the tower starts from, which Orca offsets by z_offset.
std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes,
float base_z = 0.f);
} // namespace Slic3r } // namespace Slic3r
+7 -7
View File
@@ -1032,7 +1032,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
m_y_shift(0.f), m_y_shift(0.f),
m_z_pos(0.f), m_z_pos(0.f),
m_bridging(float(config.wipe_tower_bridging)), m_bridging(float(config.wipe_tower_bridging)),
m_no_sparse_layers(config.wipe_tower_no_sparse_layers), m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
m_gcode_flavor(config.gcode_flavor), m_gcode_flavor(config.gcode_flavor),
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))), m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
m_infill_speed(default_region_config.sparse_infill_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))), m_infill_speed(default_region_config.sparse_infill_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
@@ -1730,7 +1730,7 @@ void WipeTower2::toolchange_Change(
} else if (m_wall_type == (int)wtwCone) { } else if (m_wall_type == (int)wtwCone) {
const double support_scale = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth, const double support_scale = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth,
m_wipe_tower_cone_angle).second; m_wipe_tower_cone_angle).second;
const double z = m_no_sparse_layers ? (m_current_height + m_layer_info->height) : m_layer_info->z; const double z = m_sparse_layers_skipped ? (m_current_height + m_layer_info->height) : m_layer_info->z;
const double r = std::tan(Geometry::deg2rad(m_wipe_tower_cone_angle / 2.f)) * (m_wipe_tower_height - z); const double r = std::tan(Geometry::deg2rad(m_wipe_tower_cone_angle / 2.f)) * (m_wipe_tower_height - z);
const double w = m_layer_info->depth + m_perimeter_width; const double w = m_layer_info->depth + m_perimeter_width;
if (r > 0.5 * w + 0.01) { // same guard as generate_support_cone_wall if (r > 0.5 * w + 0.01) { // same guard as generate_support_cone_wall
@@ -1872,7 +1872,7 @@ void WipeTower2::toolchange_Wipe(
// All the calculations in all other places take the spacing into account for all the layers. // All the calculations in all other places take the spacing into account for all the layers.
// If spare layers are excluded->if 1 or less toolchange has been done, it must be sill the first layer, too.So slow down. // If spare layers are excluded->if 1 or less toolchange has been done, it must be sill the first layer, too.So slow down.
const float target_speed = is_first_layer() || (m_num_tool_changes <= 1 && m_no_sparse_layers) ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f); const float target_speed = is_first_layer() || (m_num_tool_changes <= 1 && m_sparse_layers_skipped) ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
float wipe_speed = 0.33f * target_speed; float wipe_speed = 0.33f * target_speed;
// if there is less than 2.5*line_width to the edge, advance straightaway (there is likely a blob anyway) // if there is less than 2.5*line_width to the edge, advance straightaway (there is likely a blob anyway)
@@ -1970,7 +1970,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
// Slow down on the 1st layer. // Slow down on the 1st layer.
// If spare layers are excluded -> if 1 or less toolchange has been done, it must be still the first layer, too. So slow down. // If spare layers are excluded -> if 1 or less toolchange has been done, it must be still the first layer, too. So slow down.
bool first_layer = is_first_layer() || (m_num_tool_changes <= 1 && m_no_sparse_layers); bool first_layer = is_first_layer() || (m_num_tool_changes <= 1 && m_sparse_layers_skipped);
float feedrate = first_layer ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f); float feedrate = first_layer ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
if (m_enable_tower_interface_features && m_prev_layer_had_interface) if (m_enable_tower_interface_features && m_prev_layer_had_interface)
feedrate = std::min(feedrate, 20.f * 60.f); feedrate = std::min(feedrate, 20.f * 60.f);
@@ -2103,7 +2103,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
// Ask our writer about how much material was consumed. // Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (! m_no_sparse_layers || toolchanges_on_layer || first_layer) { if (! m_sparse_layers_skipped || toolchanges_on_layer || first_layer) {
if (m_current_tool < m_used_filament_length.size()) if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
m_current_height += m_layer_info->height; m_current_height += m_layer_info->height;
@@ -2226,7 +2226,7 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i
if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) // if we moved to a new layer, we'll add it to m_plan first if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) // if we moved to a new layer, we'll add it to m_plan first
m_plan.push_back(WipeTowerInfo(z_par, layer_height_par)); m_plan.push_back(WipeTowerInfo(z_par, layer_height_par));
if (m_first_layer_idx == size_t(-1) && (! m_no_sparse_layers || old_tool != new_tool || m_plan.size() == 1)) if (m_first_layer_idx == size_t(-1) && (! m_sparse_layers_skipped || old_tool != new_tool || m_plan.size() == 1))
m_first_layer_idx = m_plan.size() - 1; m_first_layer_idx = m_plan.size() - 1;
if (old_tool == new_tool) // new layer without toolchanges - we are done if (old_tool == new_tool) // new layer without toolchanges - we are done
@@ -2652,7 +2652,7 @@ Polygon WipeTower2::generate_support_cone_wall(
const auto [R, support_scale] = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth, const auto [R, support_scale] = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth,
m_wipe_tower_cone_angle); m_wipe_tower_cone_angle);
double z = m_no_sparse_layers ? double z = m_sparse_layers_skipped ?
(m_current_height + m_layer_info->height) : (m_current_height + m_layer_info->height) :
m_layer_info->z; // the former should actually work in both cases, but let's stay on the safe side (the 2.6.0 is close) m_layer_info->z; // the former should actually work in both cases, but let's stay on the safe side (the 2.6.0 is close)
+1 -1
View File
@@ -267,7 +267,7 @@ private:
float m_parking_pos_retraction = 0.f; float m_parking_pos_retraction = 0.f;
float m_extra_loading_move = 0.f; float m_extra_loading_move = 0.f;
float m_bridging = 0.f; float m_bridging = 0.f;
bool m_no_sparse_layers = false; bool m_sparse_layers_skipped = false;
bool m_set_extruder_trimpot = false; bool m_set_extruder_trimpot = false;
bool m_adhesion = true; bool m_adhesion = true;
GCodeFlavor m_gcode_flavor; GCodeFlavor m_gcode_flavor;
+1
View File
@@ -153,6 +153,7 @@ bool Layer::is_perimeter_compatible(const Print& print, const PrintRegion& a, co
&& config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) == other_config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) && config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) == other_config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id))
&& config.filter_out_gap_fill.value == other_config.filter_out_gap_fill.value && config.filter_out_gap_fill.value == other_config.filter_out_gap_fill.value
&& config.detect_overhang_wall == other_config.detect_overhang_wall && config.detect_overhang_wall == other_config.detect_overhang_wall
&& config.unsupported_wall_last == other_config.unsupported_wall_last
&& config.overhang_reverse == other_config.overhang_reverse && config.overhang_reverse == other_config.overhang_reverse
&& config.overhang_reverse_threshold == other_config.overhang_reverse_threshold && config.overhang_reverse_threshold == other_config.overhang_reverse_threshold
&& config.wall_direction == other_config.wall_direction && config.wall_direction == other_config.wall_direction
+71
View File
@@ -550,6 +550,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
if (!paths.empty()) { if (!paths.empty()) {
if (extrusion->is_closed) { if (extrusion->is_closed) {
ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour ? elrDefault : elrHole); ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour ? elrDefault : elrHole);
extrusion_loop.inset_idx = extrusion->inset_idx;
if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) == if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) ==
(pg_extrusion.is_contour || pg_extrusions.size() == 2)) (pg_extrusion.is_contour || pg_extrusions.size() == 2))
extrusion_loop.make_counter_clockwise(); extrusion_loop.make_counter_clockwise();
@@ -1318,6 +1319,73 @@ static void reorient_perimeters(ExtrusionEntityCollection &entities, bool steep_
} }
} }
// A loop made of nothing but overhang paths lies entirely off the lower layer.
static bool is_unsupported_loop(const ExtrusionEntity *entity)
{
if (!entity->is_loop())
return false;
const ExtrusionPaths &paths = static_cast<const ExtrusionLoop *>(entity)->paths;
return !paths.empty() && std::all_of(paths.begin(), paths.end(),
[](const ExtrusionPath &path) { return path.role() == erOverhangPerimeter; });
}
// ORCA: A wall loop with nothing under it has nothing to lean on, so whatever the configured wall
// sequence it is extruded after the loops that anchor it, innermost first. A loop that runs alongside
// an anchored one belongs to the same wall stack and keeps its place ahead of the infill, which needs
// it as an anchor; one that touches nothing has only that infill to rest on, so it is flagged for the
// G-code writer to hold it back until the infill is down.
static void defer_unsupported_loops(const PerimeterGenerator &perimeter_generator, ExtrusionEntityCollection &entities)
{
if (!perimeter_generator.config->unsupported_wall_last)
return;
ExtrusionEntitiesPtr &src = entities.entities;
auto first_deferred = std::stable_partition(src.begin(), src.end(),
[](const ExtrusionEntity *entity) { return !is_unsupported_loop(entity); });
if (first_deferred == src.end())
return;
std::stable_sort(first_deferred, src.end(),
[](const ExtrusionEntity *lhs, const ExtrusionEntity *rhs) { return lhs->inset_idx > rhs->inset_idx; });
auto collect_lines = [](const ExtrusionEntity *entity, Lines &out) {
Polylines polylines;
entity->collect_polylines(polylines);
append(out, to_lines(polylines));
};
Lines anchored;
for (auto it = src.begin(); it != first_deferred; ++it)
collect_lines(*it, anchored);
std::vector<ExtrusionLoop *> unattached;
for (auto it = first_deferred; it != src.end(); ++it)
unattached.emplace_back(static_cast<ExtrusionLoop *>(*it));
// A loop leaning on a loop that is itself anchored is anchored as well, so spread outwards from
// the anchored loops until no unsupported loop is left touching what was reached.
const double touch_distance = 1.5 * std::max(perimeter_generator.ext_perimeter_flow.scaled_spacing(),
perimeter_generator.perimeter_flow.scaled_spacing());
while (!anchored.empty()) {
AABBTreeLines::LinesDistancer<Line> distancer{std::move(anchored)};
anchored.clear();
for (ExtrusionLoop *&loop : unattached) {
if (loop == nullptr)
continue;
const Points points = loop->as_polyline().points;
if (std::any_of(points.begin(), points.end(),
[&distancer, touch_distance](const Point &point) { return distancer.distance_from_lines<false>(point) < touch_distance; })) {
collect_lines(loop, anchored);
loop = nullptr;
}
}
}
for (ExtrusionLoop *loop : unattached)
if (loop != nullptr)
loop->print_after_infill = true;
}
void PerimeterGenerator::process_classic() void PerimeterGenerator::process_classic()
{ {
group_region_by_fuzzify(*this); group_region_by_fuzzify(*this);
@@ -1804,6 +1872,8 @@ void PerimeterGenerator::process_classic()
} }
} }
defer_unsupported_loops(*this, entities);
// append perimeters for this slice as a collection // append perimeters for this slice as a collection
if (! entities.empty()) if (! entities.empty())
this->loops->append(entities); this->loops->append(entities);
@@ -2742,6 +2812,7 @@ void PerimeterGenerator::process_arachne()
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole, reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
this->config->overhang_reverse_internal_only); this->config->overhang_reverse_internal_only);
} }
defer_unsupported_loops(*this, extrusion_coll);
this->loops->append(extrusion_coll); this->loops->append(extrusion_coll);
} }
+4 -1
View File
@@ -1058,6 +1058,7 @@ static std::vector<std::string> s_Preset_print_options{
"reduce_crossing_wall", "reduce_crossing_wall",
"detect_thin_wall", "detect_thin_wall",
"detect_overhang_wall", "detect_overhang_wall",
"unsupported_wall_last",
"overhang_reverse", "overhang_reverse",
"overhang_reverse_threshold", "overhang_reverse_threshold",
"overhang_reverse_internal_only", "overhang_reverse_internal_only",
@@ -1320,6 +1321,8 @@ static std::vector<std::string> s_Preset_print_options{
"wipe_tower_extra_flow", "wipe_tower_extra_flow",
"single_extruder_multi_material_priming", "single_extruder_multi_material_priming",
"toolchange_ordering", "toolchange_ordering",
"toolchange_cyclic_order",
"toolchange_cyclic_first_layer",
"wipe_tower_rotation_angle", "wipe_tower_rotation_angle",
"tree_support_branch_distance_organic", "tree_support_branch_distance_organic",
"tree_support_branch_diameter_organic", "tree_support_branch_diameter_organic",
@@ -1445,7 +1448,7 @@ static std::vector<std::string> s_Preset_printer_options {
"gcode_skip_config_block", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", "gcode_skip_config_block", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
"single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode",
"printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type",
"printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", "printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", "extruder_clearance_dist_to_rod",
"nozzle_height", "master_extruder_id", "nozzle_height", "master_extruder_id",
"default_print_profile", "inherits", "default_print_profile", "inherits",
"silent_mode", "silent_mode",
+389
View File
@@ -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"
|| opt_key == "other_layers_print_sequence_nums" || opt_key == "other_layers_print_sequence_nums"
|| opt_key == "toolchange_ordering" || opt_key == "toolchange_ordering"
|| opt_key == "toolchange_cyclic_order"
|| opt_key == "toolchange_cyclic_first_layer"
|| opt_key == "extruder_ams_count" || opt_key == "extruder_ams_count"
|| opt_key == "extruder_nozzle_stats" || opt_key == "extruder_nozzle_stats"
|| opt_key == "filament_map_mode" || opt_key == "filament_map_mode"
@@ -964,6 +966,377 @@ StringObjectException Print::sequential_print_clearance_valid(const Print &print
return single_object_exception; return single_object_exception;
} }
// ---------------------------------------------------------------------------------------------
// Clearance rule for a prime tower compacted by wipe_tower_no_sparse_layers.
// Ported from BambuStudio and adapted to Orca's printer config: Orca has no
// prime_tower_lift_height (z_hop alone bounds the spiral), spells the toolhead radius
// extruder_clearance_radius, and derives the spiral slope from the per-filament travel_slope instead
// of one global constant.
// ---------------------------------------------------------------------------------------------
double compacted_tower_footprint_padding(const PrintConfig &config, double brim_width)
{
// The brim is deposited material like any other and reaches past the wall on the first layer, so
// the sweeping rod has to clear it too.
//
// On top of it, two effects make a nominal outline fall short of the printed tower on its low
// corner even though it overshoots by millimetres on the high one: WipeTower re-centres the tower
// by rib_offset once its first-layer wall is known, and the precise check hulls extrusion centre
// lines, so the deposited material reaches half a line width further still. Allowing a line width
// per side covers both, which is what keeps an estimated footprint enclosing the real one and the
// pre-slice check stricter than the precise one.
return std::max(0., brim_width) + 2. * config.nozzle_diameter.get_at(0);
}
Polygons compacted_wipe_tower_rings(const CompactedTowerZone &zone, bool any_body_tier)
{
Polygons rings = zone.grown_nozzle;
if (any_body_tier)
append(rings, zone.grown_body);
return rings;
}
CompactedTowerZone compacted_wipe_tower_zone(const PrintConfig &config, const Polygon &tower_footprint)
{
CompactedTowerZone zone;
if (tower_footprint.points.empty())
return zone;
// Spiral Z-hop at wipe-tower entry (the G3 Z I J that GCodeWriter emits for a SpiralLift) starts on
// the tower outline at a low Z. The spiral centre sits one radius away from the start point, so the
// circle reaches 2 * radius beyond the outline. radius = lift / (2*pi*atan(travel_slope)) is the
// same formula GCodeWriter uses; both are per filament, so take the widest any filament can make.
double spiral_reach = 0.;
for (size_t i = 0; i < config.z_hop.size(); ++i) {
const double lift = std::min(double(config.z_hop.get_at(i)), 5.);
if (lift < EPSILON)
continue;
const double slope = i < config.travel_slope.size() ? double(config.travel_slope.get_at(i)) : 0.;
if (slope < EPSILON)
continue;
spiral_reach = std::max(spiral_reach, 2. * lift / (2. * PI * std::atan(slope)));
}
// Working footprint = outline grown by the spiral envelope. All later clearance tests use this, so
// a travel that leaves the deposited wall at low Z is still treated as part of the tower.
zone.hull = tower_footprint;
if (spiral_reach > EPSILON) {
const Polygons grown = offset(tower_footprint, float(scale_(spiral_reach)), jtRound, scale_(0.1));
if (! grown.empty())
zone.hull = Geometry::convex_hull(grown);
}
// The rod sweeps the whole X axis, so its keep-out band is the tower's Y span widened by half
// the nozzle-to-rod offset per side (the instance carries the other half). Orca's sequential
// check has no such margin, having had no option to read it from until now.
zone.bbox_rod = zone.hull.bounding_box();
zone.bbox_rod.offset(scale_(config.extruder_clearance_dist_to_rod.value * 0.5));
// Horizontal clearance, mirroring the sequential print check down to how the distance is split:
// there each of the two object hulls grows by half of extruder_clearance_radius, so the two
// outlines touch exactly when the objects are the full radius apart. Splitting it the same way
// here (half on the tower, half on the instance in compacted_wipe_tower_clearance) states the
// same criterion, and it is what lets the plater draw both outlines: they meet at the instant the
// check trips, instead of one of them being already buried inside the other. The smaller
// MAX_OUTER_NOZZLE_DIAMETER tier is the bare nozzle cone, the only part narrow enough to sit
// beside an object rising less than nozzle_height. The 0.2 mm shaved off is the same rounding
// slack the sequential check applies, 0.1 mm per side. Both rings are built here; which one a
// given object is measured against depends on its own height and is decided in
// compacted_wipe_tower_clearance().
zone.body_radius = config.extruder_clearance_radius.value;
zone.grown_body = offset(zone.hull, float(scale_(compacted_tower_half_clearance(zone.body_radius))), jtRound, scale_(0.1));
zone.grown_nozzle = offset(zone.hull, float(scale_(compacted_tower_half_clearance(MAX_OUTER_NOZZLE_DIAMETER))), jtRound, scale_(0.1));
return zone;
}
CompactedTowerClearance compacted_wipe_tower_clearance(const PrintConfig &config, const CompactedTowerZone &zone,
const Polygon &inst_hull, double object_rise)
{
BoundingBox inst_bbox = inst_hull.bounding_box();
inst_bbox.offset(scale_(config.extruder_clearance_dist_to_rod.value * 0.5));
// Only the Y span matters for the rod: it spans the whole X axis, so an object sharing the tower's
// Y band passes under it however far apart the two are in X.
const bool overlaps_in_y = std::min(inst_bbox.max.y(), zone.bbox_rod.max.y()) - std::max(inst_bbox.min.y(), zone.bbox_rod.min.y()) > 0;
CompactedTowerClearance result;
result.far_clearance = overlaps_in_y ? config.extruder_clearance_height_to_rod.value : config.extruder_clearance_height_to_lid.value;
// The rod and the lid are the only obstacles once the object stands far enough away. Closer than
// the toolhead radius it is the head body itself that hits the object, and it does so as soon as
// the object rises past the nozzle cone, which is far below the rod.
// The instance carries the other half of each clearance, the tower rings already hold the first
// half; see compacted_wipe_tower_zone(). Both halves are needed for the verdict to mean
// "a full radius apart", and drawing what is tested is what keeps the plater honest.
//
// Which tier applies is a property of this object alone: the head body sits above the nozzle cone,
// so it cannot reach an object that stays below nozzle_height however close it stands, and however
// tall the rest of the plate is.
const bool object_is_short = object_rise <= double(config.nozzle_height.value) + EPSILON;
result.body_clearance = object_is_short ? double(MAX_OUTER_NOZZLE_DIAMETER) : zone.body_radius;
const Polygons inst_near_nozzle = offset(inst_hull, float(scale_(compacted_tower_half_clearance(MAX_OUTER_NOZZLE_DIAMETER))), jtRound, scale_(0.1));
const bool near_nozzle = ! intersection(zone.grown_nozzle, inst_near_nozzle).empty();
result.near_body = false;
if (! object_is_short) {
const Polygons inst_near_body = offset(inst_hull, float(scale_(compacted_tower_half_clearance(zone.body_radius))), jtRound, scale_(0.1));
result.near_body = ! intersection(zone.grown_body, inst_near_body).empty();
}
result.allowed_rise = result.far_clearance;
if (near_nozzle)
result.allowed_rise = 0.;
else if (result.near_body)
result.allowed_rise = std::min(result.far_clearance, double(config.nozzle_height.value));
return result;
}
Polygon compacted_wipe_tower_offender_outline(const Polygon &inst_hull, double body_clearance)
{
// Exactly the half-clearance the check grew this instance by, so the halo drawn around an object is
// the very outline that was tested against the tower ring of the same tier. Passing the clearance
// the object was actually judged on keeps a short object from being drawn with the wide ring it is
// not subject to.
const Polygons grown = offset(inst_hull, float(scale_(compacted_tower_half_clearance(body_clearance))), jtRound, scale_(0.1));
return grown.empty() ? inst_hull : grown.front();
}
// Shared user-facing message for every compacted-tower clearance failure. Height-limit and too-close
// are the same class of layout violation under "No sparse layers", so they share one wording.
static std::string compacted_wipe_tower_clearance_error()
{
return L("The relative position of the model and the prime tower does not meet the requirements of the \"No sparse layers\" feature. Please adjust their relative positions, lower the model height, or turn off \"No sparse layers\".");
}
// Convex hull of one print instance in bed coordinates, the same outline both compacted tower checks
// compare against the tower.
static Polygon compacted_tower_print_instance_hull(const PrintObject &object, const PrintInstance &instance)
{
Points pts;
for (const ModelVolume *v : object.model_object()->volumes) {
if (! v->is_model_part())
continue;
Polygon hull = v->get_convex_hull_2d(Geometry::assemble_transform(Vec3d::Zero(), instance.model_instance->get_rotation(),
instance.model_instance->get_scaling_factor(), instance.model_instance->get_mirror()));
hull.translate(instance.shift - object.center_offset());
append(pts, hull.points);
}
return pts.empty() ? Polygon() : Geometry::convex_hull(pts);
}
// Footprint the compacted prime tower is expected to occupy on the plate, in bed coordinates.
// Before psWipeTower has run there is no tower geometry at all, so this falls back to the same
// estimate the plater builds its preview box from. Answering while the user is still arranging the
// plate is the whole point of the pre-slice check, and an estimate is all that can be had then.
static Polygon estimated_wipe_tower_footprint(const Print &print)
{
const PrintConfig &config = print.config();
const size_t filaments_cnt = print.extruders().size();
if (filaments_cnt == 0)
return Polygon();
const WipeTowerData &wtd = print.wipe_tower_data(filaments_cnt);
double width, depth, brim;
Vec2d local_min;
if (wtd.bbx.size().x() > EPSILON && wtd.bbx.size().y() > EPSILON) {
// The tower has already been generated once, so use its real box (brim included) instead of
// re-estimating. Same frame first_layer_wipe_tower_corners() works in.
width = wtd.bbx.size().x();
depth = wtd.bbx.size().y();
local_min = wtd.bbx.min + wtd.rib_offset.cast<double>();
brim = 0.;
} else {
depth = wtd.depth;
if (depth < EPSILON)
return Polygon();
// PartPlate::estimate_wipe_tower_size() squares the rib tower off and the preview box the user
// drags around is built from that, so match it here rather than keeping the nominal width.
width = config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib ? depth : double(config.prime_tower_width.value);
local_min = Vec2d::Zero();
brim = double(wtd.brim_width);
}
const double padding = compacted_tower_footprint_padding(config, brim);
local_min -= Vec2d(padding, padding);
width += 2. * padding;
depth += 2. * padding;
const Eigen::Rotation2Dd rot(Geometry::deg2rad(config.wipe_tower_rotation_angle.value));
const Vec2d translate(config.wipe_tower_x.get_at(print.get_plate_index()) + print.get_plate_origin()(0),
config.wipe_tower_y.get_at(print.get_plate_index()) + print.get_plate_origin()(1));
Polygon footprint;
for (const Vec2d &corner : { local_min,
Vec2d(local_min.x() + width, local_min.y()),
Vec2d(local_min.x() + width, local_min.y() + depth),
Vec2d(local_min.x(), local_min.y() + depth) }) {
const Vec2d p = rot * corner + translate;
footprint.points.emplace_back(scale_(p.x()), scale_(p.y()));
}
return footprint;
}
// Pre-slice counterpart of validate_compacted_wipe_tower_clearance(). It applies the very same
// clearance rule, but to an estimated tower footprint instead of the real tool-change extrusions,
// which is what lets it run from Print::validate() before anything has been sliced. Reporting through
// polygons / height_polygons rather than by throwing is what puts the collision area and the height
// limit plane on the plater, exactly the way sequential printing does it.
StringObjectException Print::compacted_wipe_tower_clearance_valid(const Print &print, Polygons *polygons, std::vector<std::pair<Polygon, float>> *height_polygons)
{
const PrintConfig &config = print.config();
if (! wipe_tower_sparse_layers_skipped(config) || config.print_sequence != PrintSequence::ByLayer || ! print.has_wipe_tower())
return {};
const CompactedTowerZone zone = compacted_wipe_tower_zone(config, estimated_wipe_tower_footprint(print));
if (zone.empty())
return {};
StringObjectException exception;
Polygons offenders;
bool body_tier_used = false;
for (const PrintObject *object : print.objects()) {
const double object_top = unscaled<double>(object->max_z());
for (const PrintInstance &instance : object->instances()) {
const Polygon inst_hull = compacted_tower_print_instance_hull(*object, instance);
if (inst_hull.points.empty())
continue;
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(config, zone, inst_hull, object_top);
body_tier_used = body_tier_used || compacted_tower_body_tier(clearance);
// Every tier the precise check applies is applied here too, otherwise an object standing
// within the toolhead radius would pass here and then be rejected mid-slice, which is the
// one outcome this check exists to prevent. The compacted tower base is unknown before
// slicing, so the rise is measured from the plate rather than from the tower top; that
// overstates it by the tower's own height and makes this check err strict, never lax.
if (object_top <= clearance.allowed_rise + EPSILON)
continue;
// Height-limit and too-close cases share one user-facing message: both mean the layout
// violates the "No sparse layers" clearance rule, and the remedies are the same.
const std::string msg = compacted_wipe_tower_clearance_error();
if (exception.string.empty()) {
exception.string = msg;
exception.object = instance.model_instance;
} else {
// Same wording for every offender; keep a single copy and drop the object pointer.
exception.object = nullptr;
}
const Polygon outline = compacted_wipe_tower_offender_outline(inst_hull, clearance.body_clearance);
offenders.emplace_back(outline);
if (height_polygons)
height_polygons->emplace_back(outline, float(clearance.allowed_rise));
}
}
// Draw the tower's keep-out ring alongside the offending objects, so the collision area reads as
// "this object reaches into the space the toolhead needs around the tower" rather than as a lone
// highlighted object. Emitted only on a real collision; the plater discards polygons otherwise.
// Only the rings some object on this plate is actually measured against are drawn, so that a ring
// and an object outline touching always means that object is over its limit.
if (polygons && ! offenders.empty()) {
append(*polygons, compacted_wipe_tower_rings(zone, body_tier_used));
append(*polygons, offenders);
}
return exception;
}
// With wipe_tower_no_sparse_layers the tower only grows on layers that carry a real toolchange,
// so it ends up far below the object and the nozzle has to descend to it. While the nozzle sits
// down on the compacted tower the rod is at tower_z + extruder_clearance_height_to_rod, and it
// sweeps the tower's Y band across the whole X axis. Anything already printed above that line and
// sharing the band gets hit. Nearer than the toolhead radius the head body hits the object well before
// the rod does, which is the horizontal half of the same problem. The spiral Z-hop that opens a wipe-
// tower travel also leaves the extrusion outline at a low Z, so the footprint used here is the
// deposited hull grown by the spiral circle's maximum reach. This mirrors both clearance checks of
// sequential printing, except that the tower is revisited over and over, so every object is compared
// against it.
void Print::validate_compacted_wipe_tower_clearance() const
{
// Nothing to check when the tower is not compacted: it then follows the object as usual and the
// regular by-layer clearance check already covers it. Asking wipe_tower_sparse_layers_skipped()
// rather than the raw option keeps this from rejecting plates whose tower is in fact full height.
if (! wipe_tower_sparse_layers_skipped(m_config) || m_config.print_sequence != PrintSequence::ByLayer)
return;
const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes = m_wipe_tower_data.tool_changes;
if (tool_changes.empty() || m_objects.empty())
return;
// Same accumulation the G-code emitter runs, so validation and output cannot disagree.
const std::vector<float> tower_z = compute_compacted_wipe_tower_z(tool_changes, float(m_config.z_offset.value));
// Wipe tower footprint: build it from the ACTUAL tool-change extrusions rather than the nominal
// width x depth rectangle returned by first_layer_wipe_tower_corners(). With a rib wall the printed
// wall bulges past the nominal box and the first-layer brim reaches even further; the nominal box
// (m_wipe_tower_data.bbx) undercounts that outermost extent by several millimetres, which is
// exactly the extent that decides how close the sweeping rod comes to a neighbouring object. The
// extrusion end-points are stored in the wipe-tower local frame, so we map them to the bed frame
// with the same transform the G-code emitter applies. The two emitters differ in where rib_offset
// enters: WipeTowerIntegration::append_tcr() (type 1) rotates the point and then adds the offset,
// append_tcr2() (type 2) adds it before rotating. On a rotated rib-wall tower the two land several
// millimetres apart, which is exactly the margin this check measures, so follow the emitter in use.
const Eigen::Rotation2Dd wt_rot(Geometry::deg2rad(m_config.wipe_tower_rotation_angle.value));
const Vec2d wt_translate(m_config.wipe_tower_x.get_at(m_plate_index) + m_origin(0),
m_config.wipe_tower_y.get_at(m_plate_index) + m_origin(1));
const Vec2d rib_off = m_wipe_tower_data.rib_offset.cast<double>();
const bool rib_off_rotates = this->wipe_tower_type() == WipeTowerType::Type2;
auto to_bed = [&wt_rot, &wt_translate, &rib_off, rib_off_rotates](const Vec2d &pt) {
return rib_off_rotates ? Vec2d(wt_rot * (pt + rib_off) + wt_translate) : Vec2d(wt_rot * pt + wt_translate + rib_off);
};
Points tower_pts;
for (const std::vector<WipeTower::ToolChangeResult> &layer : tool_changes) {
if (layer.empty() || wipe_tower_layer_is_sparse(layer))
continue;
for (const WipeTower::ToolChangeResult &tcr : layer)
for (size_t i = 0; i < tcr.extrusions.size(); ++i) {
// A zero width marks a travel end-point. Keep it only when it opens a real extrusion, so
// the hull covers the deposited material and nothing else; travels reach a bit further out
// than the walls do.
const WipeTower::Extrusion &e = tcr.extrusions[i];
if (e.width == 0.f && (i + 1 == tcr.extrusions.size() || tcr.extrusions[i + 1].width == 0.f))
continue;
const Vec2d p = to_bed(Vec2d(e.pos.x(), e.pos.y()));
tower_pts.emplace_back(scale_(p.x()), scale_(p.y()));
}
}
if (tower_pts.empty())
return;
const CompactedTowerZone zone = compacted_wipe_tower_zone(m_config, Geometry::convex_hull(tower_pts));
if (zone.empty())
return;
for (const PrintObject *object : m_objects) {
const double object_top = unscaled<double>(object->max_z());
for (const PrintInstance &instance : object->instances()) {
const Polygon inst_hull = compacted_tower_print_instance_hull(*object, instance);
if (inst_hull.points.empty())
continue;
// Report the worst layer rather than the first offending one, it is the one that explains the
// collision best. The rise has to be known before the clearance: it is what selects the
// horizontal tier, the nozzle cone being out of the head body's reach.
double max_rise = 0.;
for (size_t i = 0; i < tool_changes.size(); ++i) {
if (tool_changes[i].empty() || wipe_tower_layer_is_sparse(tool_changes[i]))
continue;
// Nothing above the current layer exists yet, so a tall object only counts up to it.
const double rise = std::min(object_top, double(tool_changes[i].front().print_z)) - tower_z[i];
if (rise > max_rise)
max_rise = rise;
}
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(m_config, zone, inst_hull, max_rise);
if (max_rise <= clearance.allowed_rise + EPSILON)
continue;
// Same wording as compacted_wipe_tower_clearance_valid(): height-limit and too-close
// share one message, since both are layout violations of "No sparse layers".
throw Slic3r::SlicingError(compacted_wipe_tower_clearance_error());
}
}
}
//BBS //BBS
static StringObjectException layered_print_cleareance_valid(const Print &print, StringObjectException *warning) static StringObjectException layered_print_cleareance_valid(const Print &print, StringObjectException *warning)
{ {
@@ -1408,6 +1781,16 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
} }
if (!layer_warning.string.empty()) if (!layer_warning.string.empty())
add_warning(layer_warning); add_warning(layer_warning);
// Orca: a compacted prime tower drags the nozzle back down to the plate on every toolchange, so
// tall objects collide with it much like they do in sequential printing. Checking it here rather
// than only during slicing is what lets the plater show the collision area and the height limit
// while the plate is still being arranged.
ret = compacted_wipe_tower_clearance_valid(*this, collison_polygons, height_polygons);
if (!ret.string.empty()) {
ret.type = STRING_EXCEPT_OBJECT_COLLISION_IN_LAYER_PRINT;
return ret;
}
} }
if (m_config.enable_prime_tower) { if (m_config.enable_prime_tower) {
@@ -2620,6 +3003,12 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (this->has_wipe_tower()) { if (this->has_wipe_tower()) {
m_fake_wipe_tower.set_pos({ m_config.wipe_tower_x.get_at(m_plate_index), m_config.wipe_tower_y.get_at(m_plate_index) }); m_fake_wipe_tower.set_pos({ m_config.wipe_tower_x.get_at(m_plate_index), m_config.wipe_tower_y.get_at(m_plate_index) });
// Validated on every process() run rather than only when the wipe tower step is (re)generated.
// Moving the tower changes only wipe_tower_x/y, which invalidates psSkirtBrim but not psWipeTower,
// so a validate call living inside _make_wipe_tower would be skipped and keep using the stale
// position, missing a fresh collision. The tower geometry (tool_changes) is stored in the local
// frame and is position independent, so re-checking here with the current position is correct.
this->validate_compacted_wipe_tower_clearance();
} }
if (this->set_started(psSkirtBrim)) { if (this->set_started(psSkirtBrim)) {
+87
View File
@@ -1160,6 +1160,8 @@ public:
//BBS //BBS
static StringObjectException sequential_print_clearance_valid(const Print &print, Polygons *polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr); static StringObjectException sequential_print_clearance_valid(const Print &print, Polygons *polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr);
// Orca: pre-slice clearance check for a prime tower compacted by "No sparse layers".
static StringObjectException compacted_wipe_tower_clearance_valid(const Print &print, Polygons *polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr);
ConflictResultOpt get_conflict_result() const { return m_conflict_result; } ConflictResultOpt get_conflict_result() const { return m_conflict_result; }
// Return 4 wipe tower corners in the world coordinates (shifted and rotated), including the wipe tower brim. // Return 4 wipe tower corners in the world coordinates (shifted and rotated), including the wipe tower brim.
@@ -1174,6 +1176,8 @@ public:
void set_calib_params(const Calib_Params& params); void set_calib_params(const Calib_Params& params);
const Calib_Params& calib_params() const { return m_calib_params; } const Calib_Params& calib_params() const { return m_calib_params; }
Vec2d translate_to_print_space(const Vec2d &point) const; Vec2d translate_to_print_space(const Vec2d &point) const;
// Orca: precise counterpart of compacted_wipe_tower_clearance_valid(), run once the tower exists.
void validate_compacted_wipe_tower_clearance() const;
float get_wipe_tower_depth() const { return m_wipe_tower_data.depth; } float get_wipe_tower_depth() const { return m_wipe_tower_data.depth; }
BoundingBoxf get_wipe_tower_bbx() const { return m_wipe_tower_data.bbx; } BoundingBoxf get_wipe_tower_bbx() const { return m_wipe_tower_data.bbx; }
Vec2f get_rib_offset() const { return m_wipe_tower_data.rib_offset; } Vec2f get_rib_offset() const { return m_wipe_tower_data.rib_offset; }
@@ -1394,6 +1398,89 @@ public:
}; };
// ---------------------------------------------------------------------------------------------
// Clearance rule for a prime tower compacted by wipe_tower_no_sparse_layers. Shared by the precise
// check that runs on the real extrusions, the pre-slice estimate that feeds the plater with collision
// polygons, and the plater's own live preview while the user drags the tower or an object around.
// Keeping the rule in one place is what stops those three from drifting apart and reporting different
// things for the same plate.
// ---------------------------------------------------------------------------------------------
// Half of a clearance distance, the share each of the two outlines carries. Sequential printing splits
// extruder_clearance_radius between the two object hulls this way; the tower checks split their
// clearances between the tower ring and the instance hull for the same reason, so that the two
// outlines the plater draws touch precisely when the check trips. The 0.2 mm comes off first: it is
// the rounding slack the sequential check applies, 0.1 mm per side.
inline double compacted_tower_half_clearance(double clearance) { return 0.5 * (clearance - 0.2); }
// Keep-out geometry a compacted tower projects onto the plate, derived from its bare footprint.
struct CompactedTowerZone
{
// Footprint the checks work on: the raw outline grown by the spiral Z-hop envelope.
Polygon hull;
// hull grown by half the toolhead radius; an object whose own half-grown hull reaches into it is
// hit by the head body. This is also the ring the plater draws.
Polygons grown_body;
// hull grown by half the bare nozzle cone radius, the innermost tier.
Polygons grown_nozzle;
// hull bounding box, the Y band the rod sweeps.
BoundingBox bbox_rod;
// Full body clearance, of which grown_body carries half. Which of the two tiers applies is decided
// per object rather than here; see compacted_wipe_tower_clearance().
double body_radius { 0. };
bool empty() const { return hull.points.empty(); }
};
// Per-side padding a bare wipe tower outline needs before the clearance checks may treat it as the
// tower's footprint. Callers whose outline already carries the first-layer brim pass zero for it.
// Shared by the pre-slice estimate and the plater's live preview: both start from an outline that
// falls short of the printed tower in the same two ways, and padding them by different amounts is
// exactly how the preview and the validation behind it would end up disagreeing.
double compacted_tower_footprint_padding(const PrintConfig &config, double brim_width);
// Grow a bare tower footprint (bed frame, scaled) into its keep-out zone.
CompactedTowerZone compacted_wipe_tower_zone(const PrintConfig &config, const Polygon &tower_footprint);
// How far an object may rise above the compacted tower base before the toolhead hits it.
struct CompactedTowerClearance
{
// Height the object may reach above the tower base. Zero means it may not rise at all.
double allowed_rise;
// Clearance that applies once the object stands clear of the toolhead in XY, i.e. rod or lid.
double far_clearance;
// The object sits within the toolhead radius, so the head body limits it rather than the rod.
bool near_body;
// Horizontal clearance this particular object has to keep from the tower: the full toolhead
// radius once it rises past the nozzle cone, the bare cone while it stays below. It is what the
// error message quotes and what the plater grows the object outline by.
double body_clearance;
};
// object_rise is the height above the tower base that the caller is going to compare against
// allowed_rise. It also selects the horizontal tier, so the two cannot disagree.
CompactedTowerClearance compacted_wipe_tower_clearance(const PrintConfig &config, const CompactedTowerZone &zone,
const Polygon &inst_hull, double object_rise);
// This object was judged on a tier reaching past the bare nozzle cone, so the wide ring is the one its
// outline has to be drawn against.
inline bool compacted_tower_body_tier(const CompactedTowerClearance &clearance)
{
return clearance.body_clearance > double(MAX_OUTER_NOZZLE_DIAMETER);
}
// Keep-out rings to draw around the tower. The nozzle one always applies; the wide body one is drawn
// only when some object on the plate is actually measured against it, otherwise it would show a
// keep-out zone no object can violate.
Polygons compacted_wipe_tower_rings(const CompactedTowerZone &zone, bool any_body_tier);
// Outline to hand the plater for an offending object: the instance hull grown by the same half
// clearance the check grew it by, which is CompactedTowerClearance::body_clearance for that object.
// Sequential printing reports its hulls the same way, and it doubles as the fix for the bare hull
// being unusable on screen, where drawn flat it hides under the object and drawn at the height limit
// it ends up buried inside the mesh.
Polygon compacted_wipe_tower_offender_outline(const Polygon &inst_hull, double body_clearance);
} /* slic3r_Print_hpp_ */ } /* slic3r_Print_hpp_ */
#endif #endif
+65 -2
View File
@@ -2549,6 +2549,16 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back("5"); def->enum_labels.push_back("5");
def->mode = comAdvanced; def->mode = comAdvanced;
// Orca: already carried by the BBL/Qidi/Geeetech/Eryone machine profiles, which inherited it from
// the BambuStudio import; without a definition here it was parsed as an unknown key and dropped.
def = this->add("extruder_clearance_dist_to_rod", coFloat);
def->label = L("Distance to rod");
def->tooltip = L("Horizontal distance of the nozzle tip to the rod's farther edge. Used for collision avoidance in by-object printing.");
def->sidetext = L("mm"); // millimeters, CIS languages need translation
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(40));
def = this->add("extruder_clearance_height_to_rod", coFloat); def = this->add("extruder_clearance_height_to_rod", coFloat);
def->label = L("Height to rod"); def->label = L("Height to rod");
def->tooltip = L("Distance from the nozzle tip to the lower rod. Used for collision avoidance in by-object printing."); def->tooltip = L("Distance from the nozzle tip to the lower rod. Used for collision avoidance in by-object printing.");
@@ -5537,6 +5547,16 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(true)); def->set_default_value(new ConfigOptionBool(true));
def = this->add("unsupported_wall_last", coBool);
def->label = L("Print unsupported walls last");
def->category = L("Quality");
def->tooltip = L("Wall loops that lie entirely in mid air are printed once something can hold them:\n"
"they are extruded after the other walls of their island, innermost first, whatever the wall order is.\n"
"A loop that only the bridges of this layer can anchor waits until those bridges are printed, while a loop running "
"alongside a supported wall keeps its place before the infill, which needs it as an anchor.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("outer_wall_filament_id", coInt); def = this->add("outer_wall_filament_id", coInt);
def->gui_type = ConfigOptionDef::GUIType::i_enum_open; def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
def->label = L("Outer walls"); def->label = L("Outer walls");
@@ -6673,8 +6693,10 @@ void PrintConfigDef::init_fff_params()
def = this->add("wipe_tower_no_sparse_layers", coBool); def = this->add("wipe_tower_no_sparse_layers", coBool);
def->label = L("No sparse layers (beta)"); def->label = L("No sparse layers (beta)");
def->tooltip = L("If enabled, the wipe tower will not be printed on layers with no tool changes. " def->tooltip = L("If enabled, the wipe tower will not be printed on layers with no tool changes. "
"On layers with a tool change, extruder will travel downward to print the wipe tower. " "On layers with a tool change, extruder will travel downward to print the wipe tower, "
"User is responsible for ensuring there is no collision with the print."); "so the tower ends up below the model and the toolhead has to reach down to it. "
"Layouts where that would collide with an already printed object are rejected. "
"Has no effect with smooth timelapse or clumping detection, which need a tower on every layer.");
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false)); def->set_default_value(new ConfigOptionBool(false));
@@ -6700,6 +6722,34 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.emplace_back(L("Cyclic")); def->enum_labels.emplace_back(L("Cyclic"));
def->set_default_value(new ConfigOptionEnum<ToolChangeOrderingType>(ToolChangeOrderingType::Default)); 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 = this->add("slice_closing_radius", coFloat);
def->label = L("Slice gap closing radius"); def->label = L("Slice gap closing radius");
def->category = L("Quality"); def->category = L("Quality");
@@ -11963,6 +12013,19 @@ CLIActionsConfigDef::CLIActionsConfigDef()
"the --ground-* options choose from. Machine-readable alternative to --info."); "the --ground-* options choose from. Machine-readable alternative to --info.");
def->set_default_value(new ConfigOptionBool(false)); 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 = this->add("export_settings", coString);
def->label = L("Export Settings"); def->label = L("Export Settings");
def->tooltip = L("This exports settings to a file. Use - to write them to stdout."); def->tooltip = L("This exports settings to a file. Use - to write them to stdout.");
+4
View File
@@ -1353,6 +1353,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloatsNullable, filament_ironing_speed)) ((ConfigOptionFloatsNullable, filament_ironing_speed))
// Detect bridging perimeters // Detect bridging perimeters
((ConfigOptionBool, detect_overhang_wall)) ((ConfigOptionBool, detect_overhang_wall))
((ConfigOptionBool, unsupported_wall_last))
((ConfigOptionInt, outer_wall_filament_id)) ((ConfigOptionInt, outer_wall_filament_id))
((ConfigOptionInt, inner_wall_filament_id)) ((ConfigOptionInt, inner_wall_filament_id))
((ConfigOptionFloatOrPercent, inner_wall_line_width)) ((ConfigOptionFloatOrPercent, inner_wall_line_width))
@@ -1627,6 +1628,8 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionBool, manual_filament_change)) ((ConfigOptionBool, manual_filament_change))
((ConfigOptionBool, single_extruder_multi_material_priming)) ((ConfigOptionBool, single_extruder_multi_material_priming))
((ConfigOptionEnum<ToolChangeOrderingType>, toolchange_ordering)) ((ConfigOptionEnum<ToolChangeOrderingType>, toolchange_ordering))
((ConfigOptionString, toolchange_cyclic_order))
((ConfigOptionBool, toolchange_cyclic_first_layer))
((ConfigOptionBool, wipe_tower_no_sparse_layers)) ((ConfigOptionBool, wipe_tower_no_sparse_layers))
((ConfigOptionString, change_filament_gcode)) ((ConfigOptionString, change_filament_gcode))
((ConfigOptionString, change_extrusion_role_gcode)) ((ConfigOptionString, change_extrusion_role_gcode))
@@ -1788,6 +1791,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionBools, slow_down_for_layer_cooling)) ((ConfigOptionBools, slow_down_for_layer_cooling))
((ConfigOptionInts, close_fan_the_first_x_layers)) ((ConfigOptionInts, close_fan_the_first_x_layers))
((ConfigOptionEnum<DraftShield>, draft_shield)) ((ConfigOptionEnum<DraftShield>, draft_shield))
((ConfigOptionFloat, extruder_clearance_dist_to_rod))//BBS
((ConfigOptionFloat, extruder_clearance_height_to_rod))//BBs ((ConfigOptionFloat, extruder_clearance_height_to_rod))//BBs
((ConfigOptionFloat, extruder_clearance_height_to_lid))//BBS ((ConfigOptionFloat, extruder_clearance_height_to_lid))//BBS
((ConfigOptionFloat, extruder_clearance_radius)) ((ConfigOptionFloat, extruder_clearance_radius))
+1
View File
@@ -1501,6 +1501,7 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "fuzzy_skin_octaves" || opt_key == "fuzzy_skin_octaves"
|| opt_key == "fuzzy_skin_persistence" || opt_key == "fuzzy_skin_persistence"
|| opt_key == "detect_overhang_wall" || opt_key == "detect_overhang_wall"
|| opt_key == "unsupported_wall_last"
|| opt_key == "overhang_reverse" || opt_key == "overhang_reverse"
|| opt_key == "overhang_reverse_internal_only" || opt_key == "overhang_reverse_internal_only"
|| opt_key == "overhang_reverse_threshold" || opt_key == "overhang_reverse_threshold"
+2
View File
@@ -682,6 +682,8 @@ set(SLIC3R_GUI_SOURCES
Utils/Bonjour.hpp Utils/Bonjour.hpp
Utils/MeshInspect.cpp Utils/MeshInspect.cpp
Utils/MeshInspect.hpp Utils/MeshInspect.hpp
Utils/PaintCLI.cpp
Utils/PaintCLI.hpp
Utils/CalibUtils.cpp Utils/CalibUtils.cpp
Utils/CalibUtils.hpp Utils/CalibUtils.hpp
Utils/ColorSpaceConvert.cpp Utils/ColorSpaceConvert.cpp
+9 -2
View File
@@ -1041,10 +1041,12 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
for (auto el : {"wipe_tower_rotation_angle", "wipe_tower_cone_angle", for (auto el : {"wipe_tower_rotation_angle", "wipe_tower_cone_angle",
"wipe_tower_extra_spacing", "wipe_tower_max_purge_speed", "wipe_tower_extra_spacing", "wipe_tower_max_purge_speed",
"wipe_tower_bridging", "wipe_tower_extra_flow", "wipe_tower_bridging", "wipe_tower_extra_flow"})
"wipe_tower_no_sparse_layers"})
toggle_line(el, have_prime_tower && supports_wipe_tower_2); toggle_line(el, have_prime_tower && supports_wipe_tower_2);
// Orca: both tower generators skip sparse layers, so this is not a wipe tower 2 exclusive.
toggle_line("wipe_tower_no_sparse_layers", have_prime_tower);
WipeTowerWallType wipe_tower_wall_type = config->opt_enum<WipeTowerWallType>("wipe_tower_wall_type"); WipeTowerWallType wipe_tower_wall_type = config->opt_enum<WipeTowerWallType>("wipe_tower_wall_type");
bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower; bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower;
toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && wipe_tower_wall_type == WipeTowerWallType::wtwCone); toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && wipe_tower_wall_type == WipeTowerWallType::wtwCone);
@@ -1055,6 +1057,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); 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)); toggle_line("prime_volume",have_prime_tower && (!purge_in_primetower || !bSEMM));
for (auto el : {"flush_into_infill", "flush_into_support", "flush_into_objects"}) for (auto el : {"flush_into_infill", "flush_into_support", "flush_into_objects"})
@@ -1128,6 +1134,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
bool has_detect_overhang_wall = config->opt_bool("detect_overhang_wall"); bool has_detect_overhang_wall = config->opt_bool("detect_overhang_wall");
bool has_overhang_reverse = config->opt_bool("overhang_reverse"); bool has_overhang_reverse = config->opt_bool("overhang_reverse");
bool allow_overhang_reverse = !has_spiral_vase; bool allow_overhang_reverse = !has_spiral_vase;
toggle_line("unsupported_wall_last", has_detect_overhang_wall);
toggle_line("overhang_reverse", allow_overhang_reverse); toggle_line("overhang_reverse", allow_overhang_reverse);
toggle_line("overhang_reverse_internal_only", allow_overhang_reverse && has_overhang_reverse); toggle_line("overhang_reverse_internal_only", allow_overhang_reverse && has_overhang_reverse);
bool has_overhang_reverse_internal_only = config->opt_bool("overhang_reverse_internal_only"); bool has_overhang_reverse_internal_only = config->opt_bool("overhang_reverse_internal_only");
+11
View File
@@ -188,6 +188,17 @@ wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig&
out = double_to_string(opt->value) + (opt->percent ? "%" : ""); out = double_to_string(opt->value) + (opt->percent ? "%" : "");
return out; return out;
} }
case coFloatsOrPercents: {
const auto* values = static_cast<const ConfigOptionVector<FloatOrPercent>*>(option);
// Orca: Preset comparison may request the entire vector instead of an indexed entry.
if (orig_opt_idx < 0)
return from_u8(option->serialize());
if (opt_idx < values->size()) {
const FloatOrPercent& value = values->get_at(opt_idx);
return double_to_string(value.value) + (value.percent ? "%" : "");
}
return _L("Undefined");
}
case coEnum: { case coEnum: {
return get_string_from_enum(pure_key, config, return get_string_from_enum(pure_key, config,
pure_key == "top_surface_pattern" || pure_key == "top_surface_pattern" ||
+104 -2
View File
@@ -4316,6 +4316,9 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
if (can_sequential_clearance_show_in_gizmo()) if (can_sequential_clearance_show_in_gizmo())
update_sequential_clearance(); update_sequential_clearance();
} else { } else {
// Orca: by-layer counterpart, for a prime tower compacted by "No sparse layers".
if (current_printer_technology() == ptFFF && can_sequential_clearance_show_in_gizmo())
update_compacted_wipe_tower_clearance();
if (c == GLGizmosManager::EType::Move || if (c == GLGizmosManager::EType::Move ||
c == GLGizmosManager::EType::Scale || c == GLGizmosManager::EType::Scale ||
c == GLGizmosManager::EType::Rotate) c == GLGizmosManager::EType::Rotate)
@@ -4549,8 +4552,12 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
TransformationType trafo_type; TransformationType trafo_type;
trafo_type.set_relative(); trafo_type.set_relative();
m_selection.translate(cur_pos - m_mouse.drag.start_position_3D, trafo_type); m_selection.translate(cur_pos - m_mouse.drag.start_position_3D, trafo_type);
if (current_printer_technology() == ptFFF && (fff_print()->config().print_sequence == PrintSequence::ByObject)) if (current_printer_technology() == ptFFF) {
update_sequential_clearance(); if (fff_print()->config().print_sequence == PrintSequence::ByObject)
update_sequential_clearance();
else
update_compacted_wipe_tower_clearance();
}
// BBS // BBS
//wxGetApp().obj_manipul()->set_dirty(); //wxGetApp().obj_manipul()->set_dirty();
m_dirty = true; m_dirty = true;
@@ -5614,6 +5621,101 @@ bool GLCanvas3D::can_sequential_clearance_show_in_gizmo() {
return false; return false;
} }
// Live preview of the compacted prime tower clearance, the by-layer counterpart of
// update_sequential_clearance(). Called while the user drags a volume / gizmo; idle visibility
// matches sequential print (hidden when valid, filled when Print::validate reports a collision).
// Print::compacted_wipe_tower_clearance_valid() answers the same question authoritatively, but it
// reads the tower position from the config, which only catches up once do_move() writes it back on
// mouse release. Recomputing from the volumes here is what makes the keep-out zone follow the tower
// while it is still under the cursor.
void GLCanvas3D::update_compacted_wipe_tower_clearance()
{
if (current_printer_technology() != ptFFF)
return;
const Print *print = fff_print();
if (print == nullptr)
return;
const PrintConfig &config = print->config();
if (config.print_sequence != PrintSequence::ByLayer || ! wipe_tower_sparse_layers_skipped(config) || ! print->has_wipe_tower())
return;
PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list();
PartPlate *plate = plate_list.get_curr_plate();
if (plate == nullptr)
return;
const int plate_id = plate_list.get_curr_plate_index();
// Once the tower has been generated the scene shows its real mesh with the brim merged in,
// otherwise it is a bare estimated cube with no brim at all. Only the latter needs the brim added
// here, and the width comes from WipeTowerData, the same source the preview box is sized from, so
// the zone cannot be padded against a brim the preview was not built with.
const bool preview_carries_brim = print->is_step_done(psWipeTower) && print->wipe_tower_data().wipe_tower_mesh_data.has_value();
const double brim = preview_carries_brim ? 0. : double(print->wipe_tower_data(print->extruders().size()).brim_width);
const double padding = compacted_tower_footprint_padding(config, brim);
// Tower footprint straight from the volume the user sees, so that dragging either the tower or an
// object updates the zone on the very next frame.
Polygon tower_footprint;
for (const GLVolume *v : m_volumes.volumes) {
if (! v->is_wipe_tower || v->object_idx() - 1000 != plate_id)
continue;
const BoundingBoxf3 bbox = v->transformed_convex_hull_bounding_box();
tower_footprint = Polygon({ Point(scale_(bbox.min.x() - padding), scale_(bbox.min.y() - padding)),
Point(scale_(bbox.max.x() + padding), scale_(bbox.min.y() - padding)),
Point(scale_(bbox.max.x() + padding), scale_(bbox.max.y() + padding)),
Point(scale_(bbox.min.x() - padding), scale_(bbox.max.y() + padding)) });
break;
}
const CompactedTowerZone zone = compacted_wipe_tower_zone(config, tower_footprint);
if (zone.empty()) {
reset_sequential_print_clearance();
return;
}
// While dragging, outline every on-plate instance next to the tower ring, the way sequential print
// outlines every object. Both carry half of the clearance, so the two outlines meeting is precisely
// the moment that object goes over its limit - which is what makes the pair worth drawing at all.
// The tier is per object, so a short object gets the narrow nozzle outline rather than the wide
// body one it is not subject to; without that, a 3 mm object parked beside the tower would be drawn
// deep inside the keep-out ring while passing the check. Only the instances that already exceed
// allowed_rise also get a height limit plane.
Polygons outlines;
std::vector<std::pair<Polygon, float>> height_polygons;
bool body_tier_used = false;
const BoundingBox plate_bb = plate->get_bounding_box_crd();
for (const ModelObject *model_object : m_model->objects) {
for (size_t i = 0; i < model_object->instances.size(); ++i) {
Geometry::Transformation trafo(model_object->instances[i]->get_transformation());
const Vec3d offset = trafo.get_offset();
trafo.set_offset(Vec3d(offset.x(), offset.y(), 0.0));
const Polygon inst_hull = model_object->convex_hull_2d(trafo.get_matrix());
if (inst_hull.points.empty() || ! plate_bb.overlap(inst_hull.bounding_box()))
continue;
// Same tiers and the same rise measured from the plate as
// Print::compacted_wipe_tower_clearance_valid(), so that the preview and the validation
// that follows it never contradict each other.
const double object_top = model_object->get_instance_max_z(i);
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(config, zone, inst_hull, object_top);
body_tier_used = body_tier_used || compacted_tower_body_tier(clearance);
const Polygon outline = compacted_wipe_tower_offender_outline(inst_hull, clearance.body_clearance);
outlines.emplace_back(outline);
if (object_top <= clearance.allowed_rise + EPSILON)
continue;
height_polygons.emplace_back(outline, float(clearance.allowed_rise));
}
}
Polygons polygons = compacted_wipe_tower_rings(zone, body_tier_used);
append(polygons, outlines);
set_sequential_print_clearance_visible(true);
set_sequential_print_clearance_render_fill(false);
set_sequential_print_clearance_polygons(polygons, height_polygons);
}
void GLCanvas3D::update_sequential_clearance() void GLCanvas3D::update_sequential_clearance()
{ {
if (current_printer_technology() != ptFFF || (fff_print()->config().print_sequence == PrintSequence::ByLayer)) if (current_printer_technology() != ptFFF || (fff_print()->config().print_sequence == PrintSequence::ByLayer))
+2
View File
@@ -1191,6 +1191,8 @@ public:
bool can_sequential_clearance_show_in_gizmo(); bool can_sequential_clearance_show_in_gizmo();
void update_sequential_clearance(); void update_sequential_clearance();
// Orca: by-layer counterpart, for a prime tower compacted by "No sparse layers".
void update_compacted_wipe_tower_clearance();
const Print* fff_print() const; const Print* fff_print() const;
const SLAPrint* sla_print() const; const SLAPrint* sla_print() const;
+7 -1
View File
@@ -1053,7 +1053,13 @@ void PartPlate::render_grid(bool bottom) {
void PartPlate::render_height_limit(PartPlate::HeightLimitMode mode) void PartPlate::render_height_limit(PartPlate::HeightLimitMode mode)
{ {
if (m_print && m_print->config().print_sequence == PrintSequence::ByObject && mode != HEIGHT_LIMIT_NONE) // Orca: a prime tower compacted by "No sparse layers" drags the nozzle back down to the plate on
// every toolchange, so the rod and the lid limit how tall a neighbouring object may be exactly as
// they do in sequential printing. The reference lines are just as useful there.
const bool relevant_for_print_mode = m_print && (m_print->config().print_sequence == PrintSequence::ByObject ||
(m_print->config().print_sequence == PrintSequence::ByLayer &&
wipe_tower_sparse_layers_skipped(m_print->config()) && m_print->has_wipe_tower()));
if (relevant_for_print_mode && mode != HEIGHT_LIMIT_NONE)
{ {
// draw lower limit // draw lower limit
// ORCA: OpenGL Core Profile // ORCA: OpenGL Core Profile
+34 -16
View File
@@ -62,7 +62,7 @@ static char marker_by_type(Preset::Type type, PrinterTechnology pt)
} }
} }
std::string Option::opt_key() const { return into_u8(key).substr(2); } std::string Option::opt_key() const { return key.size() < 2 ? std::string() : into_u8(key).substr(2); }
void FoundOption::get_marked_label_and_tooltip(const char **label_, const char **tooltip_) const void FoundOption::get_marked_label_and_tooltip(const char **label_, const char **tooltip_) const
{ {
@@ -116,6 +116,7 @@ void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type ty
case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break; case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break;
case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break; case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break;
case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break; case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break;
case coFloatsOrPercents: change_opt_key<ConfigOptionVector<FloatOrPercent>>(opt_key, config, cnt); break;
case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break; case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break;
// BBS // BBS
case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break; case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
@@ -334,29 +335,46 @@ const Option &OptionsSearcher::get_option(size_t pos_in_filter) const
const Option &OptionsSearcher::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const const Option &OptionsSearcher::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const
{ {
auto not_found = [&variant_index]() -> const Option& {
static const Option empty_option;
variant_index = -2;
return empty_option;
};
variant_index = -1;
std::string opt_key2 = opt_key; std::string opt_key2 = opt_key;
if (auto n = opt_key.find('#'); n != std::string::npos) { if (auto n = opt_key.find('#'); n != std::string::npos) {
variant_index = std::atoi(opt_key.c_str() + n + 1); variant_index = std::atoi(opt_key.c_str() + n + 1);
opt_key2 = opt_key.substr(0, n); opt_key2 = opt_key.substr(0, n);
} }
auto it = std::lower_bound(options.begin(), options.end(), Option({boost::nowide::widen(get_key(opt_key2, type))})); const std::wstring key = boost::nowide::widen(get_key(opt_key2, type));
// BBS: return the 0th option when not found in searcher caused by mode difference auto it = std::lower_bound(options.begin(), options.end(), Option({key}));
// assert(it != options.end()); if (it == options.end()) return not_found();
if (it == options.end()) { variant_index = -2 ; return options[0]; } if (it->key == key) {
if (it->opt_key() == opt_key2) {
variant_index = -1; variant_index = -1;
} else { } else {
const std::string opt_key3 = opt_key2 + "#"; const std::wstring prefix = key + L"#";
it = std::lower_bound(it, options.end(), Option({boost::nowide::widen(get_key(opt_key3, type))})); it = std::lower_bound(it, options.end(), Option({prefix}));
if (it == options.end() || it->opt_key().compare(0, opt_key3.length(), opt_key3) != 0) { if (it == options.end() || it->key.compare(0, prefix.length(), prefix) != 0)
variant_index = -2; // Not found return not_found();
return options[0]; // Orca: Copy-parameters dialogs request the base key, without a vector index.
if (variant_index < 0) return *it;
const bool has_mode = type == Preset::TYPE_PRINTER && printer_options_with_variant_2.count(opt_key2) > 0;
const bool has_variant =
(type == Preset::TYPE_PRINT && print_options_with_variant.count(opt_key2) > 0) ||
(type == Preset::TYPE_FILAMENT && filament_options_with_variant.count(opt_key2) > 0) ||
(type == Preset::TYPE_PRINTER && printer_options_with_variant_1.count(opt_key2) > 0) || has_mode;
if (!has_variant || has_mode) {
// Orca: Machine limits store (Normal, Silent) pairs per variant; the UI registers only #0/#1.
const std::wstring indexed_key = has_mode ? prefix + std::to_wstring(variant_index % 2) :
boost::nowide::widen(get_key(opt_key, type));
it = std::lower_bound(it, options.end(), Option({indexed_key}));
if (it == options.end() || it->key != indexed_key)
return not_found();
if (!has_variant)
variant_index = -1;
} }
auto it2 = it;
++it2;
if (it2 != options.end() && it2->opt_key().compare(0, opt_key3.length(), opt_key3) == 0
&& printer_options_with_variant_1.find(opt_key2) == printer_options_with_variant_1.end())
variant_index = -2;
} }
return options[it - options.begin()]; return options[it - options.begin()];
+35
View File
@@ -1992,6 +1992,20 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
// reload scene to update timelapse wipe tower // reload scene to update timelapse wipe tower
if (opt_key == "timelapse_type") { if (opt_key == "timelapse_type") {
// Smooth timelapse parks the nozzle on the prime tower every layer, so it needs a tower on
// every layer. That is exactly what "No sparse layers" removes, and with both on the tower is
// planned full height and then dropped on emission. Drop "No sparse layers" and tell the user.
if (boost::any_cast<int>(value) == (int) TimelapseType::tlSmooth && m_config->opt_bool("wipe_tower_no_sparse_layers")) {
MessageDialog dlg(wxGetApp().plater(),
_L("Smooth timelapse needs a prime tower on every layer, which is not compatible with \"No sparse layers\". "
"\"No sparse layers\" has been turned off."),
_L("Warning"), wxICON_WARNING | wxOK);
dlg.ShowModal();
DynamicPrintConfig new_conf = *m_config;
new_conf.set_key_value("wipe_tower_no_sparse_layers", new ConfigOptionBool(false));
m_config_manipulation.apply(m_config, &new_conf);
}
bool wipe_tower_enabled = m_config->option<ConfigOptionBool>("enable_prime_tower")->value; bool wipe_tower_enabled = m_config->option<ConfigOptionBool>("enable_prime_tower")->value;
if (!wipe_tower_enabled && boost::any_cast<int>(value) == (int)TimelapseType::tlSmooth) { if (!wipe_tower_enabled && boost::any_cast<int>(value) == (int)TimelapseType::tlSmooth) {
MessageDialog dlg(wxGetApp().plater(), _L("A prime tower is required for smooth timelapse mode. There may be flaws on the model without prime tower. Do you want to enable the prime tower\?"), MessageDialog dlg(wxGetApp().plater(), _L("A prime tower is required for smooth timelapse mode. There may be flaws on the model without prime tower. Do you want to enable the prime tower\?"),
@@ -2007,6 +2021,23 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
} }
} }
// Mirror of the timelapse_type branch above: enabling "No sparse layers" while smooth timelapse
// is active would leave the tower on every layer anyway, so fall back to traditional timelapse.
if (opt_key == "wipe_tower_no_sparse_layers" && boost::any_cast<bool>(value)) {
auto timelapse_type = m_config->option<ConfigOptionEnum<TimelapseType>>("timelapse_type");
if (timelapse_type && timelapse_type->value == TimelapseType::tlSmooth) {
MessageDialog dlg(wxGetApp().plater(),
_L("\"No sparse layers\" is not compatible with smooth timelapse, which needs a prime tower on every layer. "
"Timelapse has been switched to traditional mode."),
_L("Warning"), wxICON_WARNING | wxOK);
dlg.ShowModal();
DynamicPrintConfig new_conf = *m_config;
new_conf.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(TimelapseType::tlTraditional));
m_config_manipulation.apply(m_config, &new_conf);
wxGetApp().plater()->update();
}
}
if (opt_key == "print_sequence" && m_config->opt_enum<PrintSequence>("print_sequence") == PrintSequence::ByObject) { if (opt_key == "print_sequence" && m_config->opt_enum<PrintSequence>("print_sequence") == PrintSequence::ByObject) {
auto printer_structure_opt = m_preset_bundle->printers.get_edited_preset().config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure"); auto printer_structure_opt = m_preset_bundle->printers.get_edited_preset().config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
if (printer_structure_opt && printer_structure_opt->value == PrinterStructure::psI3) { if (printer_structure_opt && printer_structure_opt->value == PrinterStructure::psI3) {
@@ -2763,6 +2794,7 @@ void TabPrint::build()
optgroup = page->new_optgroup(L("Overhangs"), L"param_overhang"); optgroup = page->new_optgroup(L("Overhangs"), L"param_overhang");
optgroup->append_single_option_line("detect_overhang_wall", "quality_settings_overhangs#detect-overhang-wall"); optgroup->append_single_option_line("detect_overhang_wall", "quality_settings_overhangs#detect-overhang-wall");
optgroup->append_single_option_line("unsupported_wall_last", "quality_settings_overhangs#unsupported-wall-last");
optgroup->append_single_option_line("make_overhang_printable", "quality_settings_overhangs#make-overhang-printable"); optgroup->append_single_option_line("make_overhang_printable", "quality_settings_overhangs#make-overhang-printable");
optgroup->append_single_option_line("make_overhang_printable_angle", "quality_settings_overhangs#maximum-angle"); optgroup->append_single_option_line("make_overhang_printable_angle", "quality_settings_overhangs#maximum-angle");
optgroup->append_single_option_line("make_overhang_printable_hole_size", "quality_settings_overhangs#hole-area"); optgroup->append_single_option_line("make_overhang_printable_hole_size", "quality_settings_overhangs#hole-area");
@@ -3026,6 +3058,8 @@ void TabPrint::build()
optgroup = page->new_optgroup(L("Advanced"), L"advanced"); 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("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_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("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_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"); optgroup->append_single_option_line("mmu_segmented_region_interlocking_depth", "multimaterial_settings_advanced#interlocking-depth-of-segmented-region");
@@ -5107,6 +5141,7 @@ void TabPrinter::build_fff()
optgroup = page->new_optgroup(L("Extruder Clearance"), "param_extruder_clearance"); optgroup = page->new_optgroup(L("Extruder Clearance"), "param_extruder_clearance");
optgroup->append_single_option_line("extruder_clearance_radius", "printer_basic_information_extruder_clearance#radius"); optgroup->append_single_option_line("extruder_clearance_radius", "printer_basic_information_extruder_clearance#radius");
optgroup->append_single_option_line("extruder_clearance_dist_to_rod", "printer_basic_information_extruder_clearance#distance-to-rod");
optgroup->append_single_option_line("extruder_clearance_height_to_rod", "printer_basic_information_extruder_clearance#height-to-rod"); optgroup->append_single_option_line("extruder_clearance_height_to_rod", "printer_basic_information_extruder_clearance#height-to-rod");
optgroup->append_single_option_line("extruder_clearance_height_to_lid", "printer_basic_information_extruder_clearance#height-to-lid"); optgroup->append_single_option_line("extruder_clearance_height_to_lid", "printer_basic_information_extruder_clearance#height-to-lid");
+38 -16
View File
@@ -1490,7 +1490,15 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, DynamicConfig * config
for (const std::string &opt_key : config->keys()) { for (const std::string &opt_key : config->keys()) {
int variant_index = -2; int variant_index = -2;
const Search::Option &option = searcher.get_option(opt_key, type, variant_index); Search::Option option = searcher.get_option(opt_key, type, variant_index);
if (variant_index == -2) {
// Orca: Every transferred setting must remain visible even when it is absent from the search index.
const ConfigOptionDef* def = print_config_def.get(opt_key);
const std::string label = def ? (def->full_label.empty() ? def->label : def->full_label) : std::string();
option.label_local = (label.empty() ? from_u8(opt_key) : _L(label)).ToStdWstring();
option.category_local = (def && !def->category.empty() ?
Tab::translate_category(from_u8(def->category), type) : _L("Other")).ToStdWstring();
}
auto category = option.category_local; auto category = option.category_local;
auto opt = dynamic_cast<ConfigOptionVectorBase*>(config->option(opt_key)); auto opt = dynamic_cast<ConfigOptionVectorBase*>(config->option(opt_key));
std::string value_from = opt->vserialize()[from]; std::string value_from = opt->vserialize()[from];
@@ -1518,6 +1526,8 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
else else
presets_list.emplace_back(presets_); presets_list.emplace_back(presets_);
const bool multiple_extruders = wxGetApp().preset_bundle->get_printer_extruder_count() > 1;
// Display a dialog showing the dirty options in a human readable form. // Display a dialog showing the dirty options in a human readable form.
for (PresetCollection* presets : presets_list) for (PresetCollection* presets : presets_list)
{ {
@@ -1553,29 +1563,41 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
auto variant_key = Preset::get_iot_type_string(type) + "_extruder_variant"; auto variant_key = Preset::get_iot_type_string(type) + "_extruder_variant";
auto id_key = Preset::get_iot_type_string(type) + "_extruder_id"; auto id_key = Preset::get_iot_type_string(type) + "_extruder_id";
auto extruder_variant = dynamic_cast<ConfigOptionStrings const *>(old_config.option(variant_key)); // Orca: Dirty indices belong to the edited config, which may contain newly added variants.
auto extruder_id = dynamic_cast<ConfigOptionInts const *>(old_config.option(id_key)); auto extruder_variant = dynamic_cast<ConfigOptionStrings const *>(new_config.option(variant_key));
auto extruder_id = dynamic_cast<ConfigOptionInts const *>(new_config.option(id_key));
for (const std::string& opt_key : dirty_options) { for (const std::string& opt_key : dirty_options) {
int variant_index = -2; int variant_index = -2;
const Search::Option &option = searcher.get_option(opt_key, type, variant_index); const Search::Option &option = searcher.get_option(opt_key, type, variant_index);
if (option.opt_key() != opt_key && variant_index < -1) { if (variant_index == -2) {
// When founded option isn't the correct one. // When founded option isn't the correct one.
// It can be for dirty_options: "default_print_profile", "printer_model", "printer_settings_id", // It can be for dirty_options: "default_print_profile", "printer_model", "printer_settings_id",
// because of they don't exist in searcher // because of they don't exist in searcher
continue; continue;
} }
auto category = option.category_local; wxString category = option.category_local;
if (variant_index >= 0) { wxString label = option.label_local;
if (printer_options_with_variant_2.count(opt_key.substr(0, opt_key.find_last_of('#'))) > 0) if (type == Preset::TYPE_PRINTER && variant_index >= 0 &&
variant_index /= 2; printer_options_with_variant_2.count(get_pure_opt_key(opt_key)) > 0) {
if (boost::nowide::narrow(category).find("Extruder ") == 0) // Orca: silent_mode is obsolete on import, but its option and two-column UI still exist.
category = category.substr(0, 8); // Keep mode labels for configs that explicitly enable it; omit them in the default single-mode UI.
if (extruder_id) if (new_config.opt_bool("silent_mode"))
category = category + (wxString(" {") + (extruder_id->values[variant_index] == 1 ? _L("Left: ") : _L("Right: ")) label += " (" + (variant_index % 2 == 0 ? _L("Normal") : _L("Silent")) + ")";
+ L(extruder_variant->values[variant_index]) + "}"); variant_index /= 2;
else }
category = category + (wxString(" {") + L(extruder_variant->values[variant_index]) + "}"); if (variant_index >= 0 && extruder_variant && variant_index < extruder_variant->size()) {
// Orca: Match the untranslated category and use the same extruder names as the printer tabs.
if (option.category.compare(0, 9, L"Extruder ") == 0)
category = _L("Extruder");
wxString variant_label = L(extruder_variant->values[variant_index]);
// Orca: An extruder name only disambiguates variants on printers with multiple extruders.
if (multiple_extruders && extruder_id && variant_index < extruder_id->size() && extruder_id->values[variant_index] > 0) {
const wxString extruder_name = Tab::translate_category(
wxString::Format("Extruder %d", extruder_id->values[variant_index]), Preset::TYPE_PRINTER);
variant_label = extruder_name + " (" + variant_label + ")";
}
category = variant_label + ": " + category;
} }
/*m_tree->Append(opt_key, type, option.category_local, option.group_local, option.label_local, /*m_tree->Append(opt_key, type, option.category_local, option.group_local, option.label_local,
@@ -1584,7 +1606,7 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
//PresetItem pi = {opt_key, type, 1983}; //PresetItem pi = {opt_key, type, 1983};
//m_presetitems.push_back() //m_presetitems.push_back()
PresetItem pi = {type, opt_key, category, option.group_local, option.label_local, get_string_value(opt_key, old_config), get_string_value(opt_key, new_config)}; PresetItem pi = {type, opt_key, category, option.group_local, label, get_string_value(opt_key, old_config), get_string_value(opt_key, new_config)};
m_presetitems.push_back(pi); m_presetitems.push_back(pi);
} }
+204
View File
@@ -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
+31
View File
@@ -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
+240
View File
@@ -4,9 +4,14 @@
#include "libslic3r/ExtrusionEntityCollection.hpp" #include "libslic3r/ExtrusionEntityCollection.hpp"
#include "libslic3r/Layer.hpp" #include "libslic3r/Layer.hpp"
#include "libslic3r/Print.hpp" #include "libslic3r/Print.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <limits>
#include <string>
#include <vector> #include <vector>
#include "test_helpers.hpp" #include "test_helpers.hpp"
@@ -255,3 +260,238 @@ TEST_CASE("Only one wall on the first layer needs a bottom shell", "[Perimeters]
// No bottom shell: the option is inert, down to the same walls an unchecked box gives. // No bottom shell: the option is inert, down to the same walls an unchecked box gives.
CHECK_THAT(one_wall_no_shell, Catch::Matchers::WithinAbs(plain_no_shell, 1.0)); CHECK_THAT(one_wall_no_shell, Catch::Matchers::WithinAbs(plain_no_shell, 1.0));
} }
namespace {
// The layer that closes the cavity of box_over_cavity(), the first one printed over air.
const double cavity_ceiling_z = 6.2;
// A cone standing on its tip, flaring by 5mm of radius per mm of height: at a layer height of 0.2 every
// wall of a layer lands a full millimetre outside the one below, entirely off the layer below but right
// alongside the walls printed with it.
TriangleMesh flared_cone()
{
TriangleMesh cone = make_cone(20., 4.);
cone.mirror(Z);
cone.translate(0., 0., 4.);
return cone;
}
// A 30mm box holding a 20mm cavity from z=2 to z=6, with a 4mm hole punched down through the ceiling
// of that cavity. The layer at cavity_ceiling_z bridges the cavity, and the walls of the hole sit in
// the middle of that bridge, 15mm clear of anything the layer below supports.
Print &box_over_cavity(Print &print, Model &model, const DynamicPrintConfig &config)
{
ModelObject *object = model.add_object();
object->name = "box_over_cavity.stl";
object->add_volume(make_cube(30., 30., 8.), ModelVolumeType::MODEL_PART, false);
TriangleMesh cavity = make_cube(20., 20., 4.);
cavity.translate(5.f, 5.f, 2.f);
object->add_volume(std::move(cavity), ModelVolumeType::NEGATIVE_VOLUME, false);
TriangleMesh hole = make_cube(4., 4., 6.);
hole.translate(13.f, 13.f, 5.f);
object->add_volume(std::move(hole), ModelVolumeType::NEGATIVE_VOLUME, false);
object->add_instance();
object->ensure_on_bed();
print.auto_assign_extruders(object);
print.apply(model, config);
print.validate();
print.set_status_silent();
return print;
}
// Every setting the assertions below depend on, so none of them rests on a default.
DynamicPrintConfig unsupported_walls_config(const char *wall_generator, bool unsupported_wall_last)
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "wall_generator", wall_generator },
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
{ "wall_loops", 3 },
{ "detect_overhang_wall", true },
// Outer wall first, so an unsupported loop only ends up last if the feature puts it there.
{ "wall_sequence", "outer wall/inner wall" },
{ "is_infill_first", false },
{ "sparse_infill_density", "15%" },
{ "unsupported_wall_last", unsupported_wall_last },
{ "gcode_comments", true },
});
return config;
}
// A loop extruded entirely in mid air: every one of its paths is an overhang.
bool unsupported_loop(const ExtrusionEntity *entity)
{
if (! entity->is_loop())
return false;
const ExtrusionPaths &paths = static_cast<const ExtrusionLoop *>(entity)->paths;
return ! paths.empty() && std::all_of(paths.begin(), paths.end(),
[](const ExtrusionPath &path) { return path.role() == erOverhangPerimeter; });
}
// The loops of every wall island of the print, island by island, in extrusion order.
std::vector<std::vector<const ExtrusionLoop*>> wall_islands(const Print &print)
{
std::vector<std::vector<const ExtrusionLoop*>> islands;
for (const Layer *layer : print.objects().front()->layers())
for (const LayerRegion *region : layer->regions())
for (const ExtrusionEntity *island : region->perimeters.entities) {
std::vector<const ExtrusionLoop*> loops;
for (const ExtrusionEntity *entity : static_cast<const ExtrusionEntityCollection*>(island)->entities)
if (entity->is_loop())
loops.push_back(static_cast<const ExtrusionLoop*>(entity));
islands.push_back(std::move(loops));
}
return islands;
}
// Islands where a loop that is anchored is extruded after one that is not.
int islands_with_a_supported_loop_last(const Print &print)
{
int count = 0;
for (const std::vector<const ExtrusionLoop*> &loops : wall_islands(print)) {
bool seen_unsupported = false;
for (const ExtrusionLoop *loop : loops) {
if (unsupported_loop(loop))
seen_unsupported = true;
else if (seen_unsupported) {
++ count;
break;
}
}
}
return count;
}
// The unsupported loops of the print, and those of them held back for the infill.
std::vector<const ExtrusionLoop*> unsupported_loops(const Print &print, double print_z = -1.)
{
std::vector<const ExtrusionLoop*> loops;
for (const Layer *layer : print.objects().front()->layers()) {
if (print_z >= 0. && std::abs(layer->print_z - print_z) > EPSILON)
continue;
for (const LayerRegion *region : layer->regions())
for (const ExtrusionEntity *island : region->perimeters.entities)
for (const ExtrusionEntity *entity : static_cast<const ExtrusionEntityCollection*>(island)->entities)
if (unsupported_loop(entity))
loops.push_back(static_cast<const ExtrusionLoop*>(entity));
}
return loops;
}
int loops_held_back_for_infill(const std::vector<const ExtrusionLoop*> &loops)
{
return int(std::count_if(loops.begin(), loops.end(), [](const ExtrusionLoop *loop) { return loop->print_after_infill; }));
}
// The G-code emitted at `print_z`, so the order of one layer can be read on its own.
std::string layer_gcode(const std::string &gcode, double print_z)
{
std::string out;
GCodeReader reader;
reader.parse_buffer(gcode, [&out, print_z](GCodeReader &self, const GCodeReader::GCodeLine &line) {
if (std::abs(self.z() - print_z) < EPSILON)
out += line.raw() + "\n";
});
return out;
}
} // namespace
// Whatever the wall order asks for, a loop with nothing under it cannot be extruded before the loops it
// leans on. The flared cone gives every layer an outer wall that lands completely off the one below, and
// the outer wall first sequence would otherwise put it down before any of them.
TEST_CASE("Unsupported wall loops are extruded after the walls that anchor them", "[Perimeters]")
{
const char *wall_generator = GENERATE("classic", "arachne");
CAPTURE(wall_generator);
auto slice_cone = [wall_generator](bool unsupported_wall_last, Print &print) {
init_and_process_print({ flared_cone() }, print, unsupported_walls_config(wall_generator, unsupported_wall_last));
REQUIRE_FALSE(print.objects().empty());
};
Print on;
slice_cone(true, on);
// Without unsupported loops to reorder the rest of the test would pass on an empty print.
REQUIRE(unsupported_loops(on).size() > 0);
CHECK(islands_with_a_supported_loop_last(on) == 0);
SECTION("the held back loops run innermost first") {
for (const std::vector<const ExtrusionLoop*> &loops : wall_islands(on)) {
int previous_inset = std::numeric_limits<int>::max();
for (const ExtrusionLoop *loop : loops)
if (unsupported_loop(loop)) {
CHECK(loop->inset_idx <= previous_inset);
previous_inset = loop->inset_idx;
}
}
}
SECTION("switched off, the configured wall order is left alone") {
Print off;
slice_cone(false, off);
REQUIRE(unsupported_loops(off).size() == unsupported_loops(on).size());
// Outer wall first puts the unsupported outer wall ahead of the walls behind it.
CHECK(islands_with_a_supported_loop_last(off) > 0);
}
}
// A loop the walls cannot reach is a different case: only the bridges of its own layer will ever hold it,
// so it has to wait for them - while a loop that runs alongside a wall keeps its place, because the
// bridges anchor on it instead.
TEST_CASE("A wall loop out of reach of the layer below waits for the infill", "[Perimeters]")
{
const char *wall_generator = GENERATE("classic", "arachne");
CAPTURE(wall_generator);
Print print;
Model model;
box_over_cavity(print, model, unsupported_walls_config(wall_generator, true));
print.process();
const std::vector<const ExtrusionLoop*> hole_loops = unsupported_loops(print, cavity_ceiling_z);
REQUIRE(hole_loops.size() > 0);
CHECK(loops_held_back_for_infill(hole_loops) == int(hole_loops.size()));
SECTION("a loop alongside a supported wall is not held back") {
Print cone;
init_and_process_print({ flared_cone() }, cone, unsupported_walls_config(wall_generator, true));
const std::vector<const ExtrusionLoop*> loops = unsupported_loops(cone);
REQUIRE(loops.size() > 0);
CHECK(loops_held_back_for_infill(loops) == 0);
}
SECTION("switched off, no loop is held back") {
Print off;
Model off_model;
box_over_cavity(off, off_model, unsupported_walls_config(wall_generator, false));
off.process();
const std::vector<const ExtrusionLoop*> loops = unsupported_loops(off, cavity_ceiling_z);
REQUIRE(loops.size() == hole_loops.size());
CHECK(loops_held_back_for_infill(loops) == 0);
}
}
// The held back loops reach the G-code in a second pass, after the infill of their layer: on the layer
// that closes the cavity the walls of the hole are extruded once the bridge is down, so the layer emits
// perimeters, then infill, then the perimeters that were waiting for it.
TEST_CASE("Loops waiting for the infill are extruded after it", "[Perimeters]")
{
const char *wall_generator = GENERATE("classic", "arachne");
CAPTURE(wall_generator);
auto ceiling_roles = [wall_generator](bool unsupported_wall_last) {
Print print;
Model model;
box_over_cavity(print, model, unsupported_walls_config(wall_generator, unsupported_wall_last));
const std::string layer = layer_gcode(gcode(print), cavity_ceiling_z);
REQUIRE_FALSE(layer.empty());
return role_sequence(layer, { "perimeter", "infill" });
};
CHECK(ceiling_roles(true) == std::vector<std::string>{ "perimeter", "infill", "perimeter" });
CHECK(ceiling_roles(false) == std::vector<std::string>{ "perimeter", "infill" });
}
+17
View File
@@ -33,3 +33,20 @@ TEST_CASE("deep_diff flags new vector entries that duplicate values[0]", "[Prese
// specific to new indices rather than flagging the whole vector. // specific to new indices rather than flagging the whole vector.
REQUIRE(std::find(diff.begin(), diff.end(), "nozzle_diameter#0") == diff.end()); REQUIRE(std::find(diff.begin(), diff.end(), "nozzle_diameter#0") == diff.end());
} }
TEST_CASE("deep_diff distinguishes absolute and percentage speeds for each variant", "[PresetDiff][Config]")
{
const size_t changed_index = GENERATE(size_t(0), size_t(1));
Preset reference(Preset::TYPE_PRINT, "ref");
reference.config.set_key_value("small_perimeter_speed", new ConfigOptionFloatsOrPercents{{50., false}, {50., false}});
Preset edited = reference;
edited.config.option<ConfigOptionFloatsOrPercents>("small_perimeter_speed")->values[changed_index].percent = true;
const auto diff = PresetCollection::dirty_options(&edited, &reference, /*deep_compare=*/true);
REQUIRE(diff == std::vector<std::string>{"small_perimeter_speed#" + std::to_string(changed_index)});
DynamicPrintConfig transferred = reference.config;
transferred.apply_only(edited.config, diff);
REQUIRE(*transferred.option("small_perimeter_speed") == *edited.config.option("small_perimeter_speed"));
}
@@ -1025,3 +1025,41 @@ TEST_CASE("Selector slicing keeps the result valid across re-apply", "[Print][H2
REQUIRE(status != PrintBase::APPLY_STATUS_INVALIDATED); REQUIRE(status != PrintBase::APPLY_STATUS_INVALIDATED);
REQUIRE(print.is_step_done(psSlicingFinished)); 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}));
}
}
+196
View File
@@ -6,6 +6,8 @@
#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/GCode/WipeTower.hpp" #include "libslic3r/GCode/WipeTower.hpp"
#include "libslic3r/GCode/WipeTower2.hpp" #include "libslic3r/GCode/WipeTower2.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/PrintConfig.hpp"
using namespace Slic3r; using namespace Slic3r;
using Catch::Matchers::WithinAbs; using Catch::Matchers::WithinAbs;
@@ -91,3 +93,197 @@ TEST_CASE("Brim width estimate matches each generator's loop quantization", "[Wi
CHECK_THAT(WipeTower::estimate_brim_real_width(3.f, 0.4f, 0.2f, false), WithinAbs(7.5f * spacing, 1e-4f)); CHECK_THAT(WipeTower::estimate_brim_real_width(3.f, 0.4f, 0.2f, false), WithinAbs(7.5f * spacing, 1e-4f));
CHECK_THAT(WipeTower::estimate_brim_real_width(0.f, 0.4f, 0.2f, true), WithinAbs(0.f, 1e-6f)); CHECK_THAT(WipeTower::estimate_brim_real_width(0.f, 0.4f, 0.2f, true), WithinAbs(0.f, 1e-6f));
} }
// ---------------------------------------------------------------------------------------------
// "No sparse layers": the compaction rule and the clearance it demands of the plate.
// ---------------------------------------------------------------------------------------------
// A square of side mm centred on (cx, cy), in bed coordinates.
static Polygon centered_square(double cx, double cy, double side)
{
const double h = 0.5 * side;
Polygon poly;
poly.points = {Point::new_scale(cx - h, cy - h), Point::new_scale(cx + h, cy - h),
Point::new_scale(cx + h, cy + h), Point::new_scale(cx - h, cy + h)};
return poly;
}
static WipeTower::ToolChangeResult make_tcr(int initial_tool, int new_tool, float layer_height)
{
WipeTower::ToolChangeResult tcr{};
tcr.initial_tool = initial_tool;
tcr.new_tool = new_tool;
tcr.layer_height = layer_height;
return tcr;
}
// A 20 mm square tower at the bed origin, no spiral z-hop, so the keep-out zone is the bare
// footprint and every distance below is one the test sets.
static PrintConfig clearance_config()
{
PrintConfig cfg;
cfg.extruder_clearance_radius.value = 40.;
cfg.extruder_clearance_dist_to_rod.value = 20.;
cfg.extruder_clearance_height_to_rod.value = 25.;
cfg.extruder_clearance_height_to_lid.value = 120.;
cfg.nozzle_height.value = 5.;
cfg.nozzle_diameter.values = {0.4};
cfg.z_hop.values = {0.};
cfg.travel_slope.values = {3.};
return cfg;
}
TEST_CASE("Sparse layers are skipped only when nothing else needs a tower on every layer", "[WipeTower][NoSparseLayers]") {
PrintConfig cfg;
cfg.timelapse_type.value = TimelapseType::tlTraditional;
cfg.enable_wrapping_detection.value = false;
cfg.wipe_tower_no_sparse_layers.value = false;
CHECK_FALSE(wipe_tower_sparse_layers_skipped(cfg));
cfg.wipe_tower_no_sparse_layers.value = true;
CHECK(wipe_tower_sparse_layers_skipped(cfg));
// Both park the nozzle on the tower every layer, so no layer is ever dropped and the option
// must read as off everywhere rather than compact in one place and not another.
cfg.timelapse_type.value = TimelapseType::tlSmooth;
CHECK_FALSE(wipe_tower_sparse_layers_skipped(cfg));
cfg.timelapse_type.value = TimelapseType::tlTraditional;
cfg.enable_wrapping_detection.value = true;
CHECK_FALSE(wipe_tower_sparse_layers_skipped(cfg));
}
TEST_CASE("A planned layer is sparse only when its single tool change keeps the filament", "[WipeTower][NoSparseLayers]") {
CHECK(wipe_tower_layer_is_sparse({make_tcr(1, 1, 0.2f)}));
CHECK_FALSE(wipe_tower_layer_is_sparse({make_tcr(0, 1, 0.2f)}));
// A second entry means the layer carries real work whatever the tools are.
CHECK_FALSE(wipe_tower_layer_is_sparse({make_tcr(1, 1, 0.2f), make_tcr(1, 1, 0.2f)}));
CHECK_FALSE(wipe_tower_layer_is_sparse({}));
}
TEST_CASE("The compacted tower falls one layer height behind the object per sparse layer", "[WipeTower][NoSparseLayers]") {
// Five 0.2 mm layers off a 0.1 mm z offset, the middle two sparse. The object reaches
// 0.1 + 5 * 0.2 = 1.1; the tower only grows on the three printed layers, so it ends at
// 0.1 + 3 * 0.2 = 0.7 and a sparse layer carries the previous value rather than its own.
const std::vector<std::vector<WipeTower::ToolChangeResult>> tool_changes{
{make_tcr(0, 1, 0.2f)}, {make_tcr(1, 1, 0.2f)}, {make_tcr(1, 1, 0.2f)},
{make_tcr(1, 0, 0.2f)}, {make_tcr(0, 1, 0.2f)}};
const std::vector<float> tower_z = compute_compacted_wipe_tower_z(tool_changes, 0.1f);
REQUIRE(tower_z.size() == tool_changes.size());
CHECK_THAT(tower_z[0], WithinAbs(0.3f, 1e-5f));
CHECK_THAT(tower_z[1], WithinAbs(0.3f, 1e-5f));
CHECK_THAT(tower_z[2], WithinAbs(0.3f, 1e-5f));
CHECK_THAT(tower_z[3], WithinAbs(0.5f, 1e-5f));
CHECK_THAT(tower_z[4], WithinAbs(0.7f, 1e-5f));
CHECK_THAT(1.1f - tower_z.back(), WithinAbs(2 * 0.2f, 1e-5f));
// Without a base the tower starts at the bed, and an empty layer carries over like a sparse one.
const std::vector<float> no_offset = compute_compacted_wipe_tower_z({{make_tcr(0, 1, 0.2f)}, {}}, 0.f);
CHECK_THAT(no_offset[0], WithinAbs(0.2f, 1e-5f));
CHECK_THAT(no_offset[1], WithinAbs(0.2f, 1e-5f));
}
TEST_CASE("The tower keep-out zone grows by the spiral z-hop envelope", "[WipeTower][NoSparseLayers]") {
PrintConfig cfg = clearance_config();
const Polygon footprint = centered_square(0., 0., 20.);
// No lift, no envelope: the zone works on the bare footprint.
CHECK_THAT(unscaled(compacted_wipe_tower_zone(cfg, footprint).hull.bounding_box().max.x()), WithinAbs(10., 1e-6));
// A spiral lift leaves the outline at low z, so it counts as tower. The circle reaches
// 2 * lift / (2*pi*atan(slope)) past the outline, matching GCodeWriter: 2*2/(2*pi*atan(3)) = 0.51 mm.
cfg.z_hop.values = {2.};
const CompactedTowerZone lifted = compacted_wipe_tower_zone(cfg, footprint);
CHECK_THAT(unscaled(lifted.hull.bounding_box().max.x()), WithinAbs(10.51, 0.02));
CHECK_THAT(unscaled(lifted.hull.bounding_box().min.y()), WithinAbs(-10.51, 0.02));
CHECK(diff(Polygons{footprint}, Polygons{lifted.hull}).empty());
// z_hop is capped at 5 mm by the option, so a taller lift cannot widen the zone further.
cfg.z_hop.values = {10.};
const double capped = unscaled(compacted_wipe_tower_zone(cfg, footprint).hull.bounding_box().max.x());
CHECK_THAT(capped, WithinAbs(10. + 2. * 5. / (2. * M_PI * std::atan(3.)), 0.02));
// The rod sweeps the whole X axis, so its band is the tower's y span plus half the rod offset.
CHECK_THAT(unscaled(lifted.bbox_rod.max.y()), WithinAbs(10.51 + 10., 0.02));
}
TEST_CASE("An object beside a compacted tower is limited by the nearest part of the toolhead", "[WipeTower][NoSparseLayers]") {
const PrintConfig cfg = clearance_config();
const CompactedTowerZone zone = compacted_wipe_tower_zone(cfg, centered_square(0., 0., 20.));
// Each side carries half its clearance less 0.1 mm slack, so the two outlines meet when the
// objects are a full clearance apart: 2 * (4 - 0.2) / 2 = 3.8 mm for the bare nozzle cone,
// 2 * (40 - 0.2) / 2 = 39.8 mm for the head body. A 10 mm object at x leaves a gap of x - 15.
const double tall = 50., shortish = 3.;
// Gap 1 mm, inside the nozzle cone: the object may not rise above the tower at all.
const CompactedTowerClearance touching = compacted_wipe_tower_clearance(cfg, zone, centered_square(16., 0., 10.), tall);
CHECK_THAT(touching.allowed_rise, WithinAbs(0., 1e-9));
// Gap 10 mm: clear of the cone but inside the head body, which starts at nozzle_height.
const CompactedTowerClearance near_body = compacted_wipe_tower_clearance(cfg, zone, centered_square(25., 0., 10.), tall);
CHECK(near_body.near_body);
CHECK_THAT(near_body.allowed_rise, WithinAbs(5., 1e-9));
CHECK_THAT(near_body.body_clearance, WithinAbs(40., 1e-9));
// The same spot, but an object that never rises past the cone. The body sits above the cone, so
// it cannot reach this object however close it stands, and only the narrow tier applies.
const CompactedTowerClearance low = compacted_wipe_tower_clearance(cfg, zone, centered_square(25., 0., 10.), shortish);
CHECK_FALSE(low.near_body);
CHECK_THAT(low.body_clearance, WithinAbs(4., 1e-9));
CHECK_THAT(low.allowed_rise, WithinAbs(25., 1e-9));
// Gap 55 mm, clear of the head entirely: the rod is the obstacle, since the object shares the
// tower's y band and the rod spans the whole x axis however far apart the two stand.
const CompactedTowerClearance far_in_band = compacted_wipe_tower_clearance(cfg, zone, centered_square(70., 0., 10.), tall);
CHECK_FALSE(far_in_band.near_body);
CHECK_THAT(far_in_band.far_clearance, WithinAbs(25., 1e-9));
CHECK_THAT(far_in_band.allowed_rise, WithinAbs(25., 1e-9));
// Out of the band the rod passes over it and only the lid is left.
const CompactedTowerClearance out_of_band = compacted_wipe_tower_clearance(cfg, zone, centered_square(70., 60., 10.), tall);
CHECK_THAT(out_of_band.allowed_rise, WithinAbs(120., 1e-9));
}
TEST_CASE("The ring drawn around the tower meets the outline drawn around an offender", "[WipeTower][NoSparseLayers]") {
const PrintConfig cfg = clearance_config();
const CompactedTowerZone zone = compacted_wipe_tower_zone(cfg, centered_square(0., 0., 20.));
// What the plater draws has to be what the check tested, otherwise a user moves an object until
// the outlines part and slicing still refuses the plate. Both halves of the 3.8 mm nozzle
// clearance: at a 3 mm gap the rings overlap and the rise limit is zero, at 5 mm neither holds.
for (const auto &c : {std::make_pair(18., true), std::make_pair(20., false)}) {
DYNAMIC_SECTION("object at x = " << c.first) {
const Polygon hull = centered_square(c.first, 0., 10.);
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(cfg, zone, hull, 3.);
const Polygons rings = compacted_wipe_tower_rings(zone, compacted_tower_body_tier(clearance));
const Polygon outline = compacted_wipe_tower_offender_outline(hull, clearance.body_clearance);
const bool outlines_meet = ! intersection(rings, Polygons{outline}).empty();
const bool rise_denied = clearance.allowed_rise < EPSILON;
CHECK(outlines_meet == c.second);
CHECK(rise_denied == c.second);
}
}
}
TEST_CASE("Only the keep-out ring an object is measured against is drawn", "[WipeTower][NoSparseLayers]") {
const PrintConfig cfg = clearance_config();
const CompactedTowerZone zone = compacted_wipe_tower_zone(cfg, centered_square(0., 0., 20.));
// Drawing the wide ring when no object is judged on it would show a keep-out zone the check can
// never trip, so it is added only once some object reaches past the nozzle cone.
CHECK(compacted_wipe_tower_rings(zone, false).size() == zone.grown_nozzle.size());
CHECK(compacted_wipe_tower_rings(zone, true).size() == zone.grown_nozzle.size() + zone.grown_body.size());
CHECK_THAT(unscaled(get_extents(zone.grown_nozzle).max.x()), WithinAbs(10. + 0.5 * (4. - 0.2), 0.02));
CHECK_THAT(unscaled(get_extents(zone.grown_body).max.x()), WithinAbs(10. + 0.5 * (40. - 0.2), 0.02));
}
TEST_CASE("Footprint padding covers the brim and the extrusion half width on each side", "[WipeTower][NoSparseLayers]") {
// A nominal outline hulls extrusion centre lines and is re-centred once the real wall is known,
// so a line width per side on top of the brim is what keeps an estimate enclosing the real tower.
const PrintConfig cfg = clearance_config();
CHECK_THAT(compacted_tower_footprint_padding(cfg, 2.), WithinAbs(2. + 2. * 0.4, 1e-9));
CHECK_THAT(compacted_tower_footprint_padding(cfg, 0.), WithinAbs(2. * 0.4, 1e-9));
// Callers whose outline already carries the brim pass zero, and a negative one cannot shrink it.
CHECK_THAT(compacted_tower_footprint_padding(cfg, -5.), WithinAbs(2. * 0.4, 1e-9));
}