Merge branch 'main' into feature/filament_id

Brings in 560 commits (merge base 2026-07-04 .. origin/main af9fd10d7a).
19 conflicts resolved; the other 138 touched files auto-merged.

Conflict resolutions
- Snapmaker/Polymaker (7): main normalised JSON key order in #15039 while this
  branch inserted filament_id/filament_vendor. Took main's key order and kept
  this branch's values, inserting a single filament_id key rather than letting
  git's line merge leave duplicate "from"/"instantiation" keys.
- re3D rPETG @0.8/@1.75 nozzle (2): main renamed these from "re3D Greengate
  rPETG @*" and re-vendored GreenGate3D -> re3D (#15169). Git's rename-aware
  merge produced a file with two filament_vendor keys; took main's version
  wholesale instead. The id is reconciled by the tooling, not by hand.
- re3D rPP (1): kept main's inherits change (fdm_filament_pet -> fdm_filament_pp,
  local filament_type override dropped) and its widened compatible_printers.
  The flattened type is still PP, so the product triple and id OFfHGM1D are
  unchanged.
- re3D Greengate rPETG.json, Afinia {ABS+,ABS,PLA,TPU,Value ABS,Value PLA}.json
  (7 modify/delete): accepted main's deletions. The Afinia ids are not orphaned
  - the surviving @HS presets resolve the same triple and already carry the
  same ids, so nothing is retired.
- .github/workflows/check_profiles.yml: kept both main's new "validate slice"
  step and this branch's tree-wide filament-subtype check.
- tests/libslic3r/CMakeLists.txt: kept both test_filament_id_succession.cpp and
  main's test_preset_diff.cpp.

Verified
- No conflict markers; all 12795 profile JSONs parse with no duplicate keys.
- All branch artifacts (tooling, snapshot, ledger, doc, tests) intact and
  unmodified by the merge.
- The succession-ledger C++ call sites survived main's refactors; audited
  Preset.{cpp,hpp}, PresetBundle.cpp, DeviceManager.cpp, PresetComboBoxes.cpp,
  CaliHistoryDialog.cpp, MoonrakerPrinterAgent.cpp against base/ours/theirs.
- scripts/tests: 115/116 pass.

Known follow-up: assign_filament_ids.py --check reports 243 errors, all from
filament data main added in the last month (BBL/addnorth, Qidi X5/Plus 5,
Snapmaker U1, re3D). The single failing unit test is the live-tree conformance
test asserting that count is zero. Reconciliation lands separately.
This commit is contained in:
SoftFever
2026-08-08 02:05:37 +08:00
2224 changed files with 2621810 additions and 1287330 deletions
+74 -4
View File
@@ -88,6 +88,10 @@ if (SLIC3R_GUI)
list(FILTER wxWidgets_LIBRARIES EXCLUDE REGEX OpenGL)
# list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc)
find_package(wxInspector REQUIRED)
list(APPEND wxWidgets_LIBRARIES "wxInspector::wxInspector")
message(STATUS "wx libs: ${wxWidgets_LIBRARIES}")
add_subdirectory(slic3r)
@@ -129,6 +133,23 @@ if (MINGW)
set_target_properties(OrcaSlicer PROPERTIES PREFIX "")
endif (MINGW)
# The GUI embeds the bundled shared libpython (libslic3r_gui -> pybind11::embed);
# the dep recipe stamps it with an @rpath id / plain SONAME, so the executable
# needs an rpath entry for each layout in the "Bundled Python/uv layout" map
# further down. Dead entries are skipped by the loader.
# OrcaSlicer_profile_validator links libslic3r only (no Python).
if (APPLE)
# A linker flag rather than the BUILD_RPATH property because the release
# flow uses the Xcode generator, which does not honor BUILD_RPATH.
target_link_options(OrcaSlicer PRIVATE "LINKER:-rpath,@executable_path/python/lib")
elseif (UNIX AND NOT WIN32)
set_target_properties(OrcaSlicer PROPERTIES
# Build tree + AppImage, which packages the build-tree binary.
BUILD_RPATH "$ORIGIN/python/lib;$ORIGIN/../lib/python/lib"
# Flatpak (cmake --install).
INSTALL_RPATH "$ORIGIN/../libpython/lib")
endif ()
if (NOT WIN32 AND NOT APPLE)
# Binary name on unix like systems (Linux, Unix)
set_target_properties(OrcaSlicer PROPERTIES OUTPUT_NAME "orca-slicer")
@@ -197,6 +218,26 @@ endif ()
set(output_dlls_Release "")
set(output_dlls_Debug "")
set(output_dlls_RelWithDebInfo "")
# ---- Bundled Python/uv layout (canonical map) ---------------------------
# The runtime and uv are staged three ways because packaging differs:
# install() rules - Windows installer; on FHS/Flatpak they stage
# only uv (Flatpak's runtime comes from the deps
# build installing to /app/libpython; plain FHS
# installs bundle no python runtime)
# POST_BUILD copies (here) - build-tree runs, and the macOS .app/AppImage
# flows, which package the build tree and
# never run install()
# packaging scripts - build_release_macos.sh (copies the .app),
# build_linux_image.sh.in (assembles $APPDIR)
# Final locations relative to the executable:
# Windows: <exe dir>/python <resources>/tools/uv
# macOS: Contents/MacOS/python Contents/MacOS/tools/uv
# AppImage: $APPDIR/lib/python $APPDIR/resources/tools/uv
# Flatpak: /app/libpython (dep prefix) /app/share/OrcaSlicer/tools/uv
# Runtime lookup: PythonInterpreter::initialize() / bundled_uv_path().
# libpython resolution: rpath on the OrcaSlicer target (above); the dep
# recipe gives the bundled interpreter a self-relative rpath.
# --------------------------------------------------------------------------
if (WIN32)
# This has to be a separate target due to the windows command line length limits
add_custom_target(COPY_DLLS ALL DEPENDS OrcaSlicer)
@@ -231,6 +272,12 @@ if (WIN32)
VERBATIM
)
endif ()
# copy libpython to the bin folder for Windows
add_custom_command(TARGET OrcaSlicer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -rf "$<TARGET_FILE_DIR:OrcaSlicer>/python"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_PREFIX_PATH}/libpython" "$<TARGET_FILE_DIR:OrcaSlicer>/python"
COMMENT "Copying Python runtime into the build tree"
VERBATIM)
else ()
@@ -240,33 +287,55 @@ else ()
COMMAND ln -sf OrcaSlicer orca-slicer
WORKING_DIRECTORY "$<TARGET_FILE_DIR:OrcaSlicer>"
VERBATIM)
else ()
add_custom_command(TARGET OrcaSlicer POST_BUILD
WORKING_DIRECTORY "$<TARGET_FILE_DIR:OrcaSlicer>"
VERBATIM)
endif ()
if (XCODE)
# Because of Debug/Release/etc. configurations (similar to MSVC) the slic3r binary is located in an extra level
set(BIN_RESOURCES_DIR "${CMAKE_CURRENT_BINARY_DIR}/resources")
set(BIN_DIR "${CMAKE_CURRENT_BINARY_DIR}")
set(BIN_CONF_DIR "Debug")
else ()
set(BIN_RESOURCES_DIR "${CMAKE_CURRENT_BINARY_DIR}/../resources")
set(BIN_DIR "$<TARGET_FILE_DIR:OrcaSlicer>")
endif ()
if (CMAKE_MACOSX_BUNDLE)
if (CMAKE_CONFIGURATION_TYPES)
set(BIN_RESOURCES_DIR "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/OrcaSlicer.app/Contents/Resources")
set(BIN_DIR "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/OrcaSlicer.app/Contents/MacOS")
else()
set(BIN_RESOURCES_DIR "${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.app/Contents/Resources")
set(BIN_DIR "${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.app/Contents/MacOS")
endif()
set(MACOSX_BUNDLE_ICON_FILE Icon.icns)
set(MACOSX_BUNDLE_BUNDLE_NAME "OrcaSlicer")
set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${SoftFever_VERSION})
set(MACOSX_BUNDLE_COPYRIGHT "Copyright(C) 2022-2024 Li Jiang All Rights Reserved")
if (XCODE)
# Xcode's CodeSign phase fails on the bundled Python runtime's dotted
# dirs (python3.12) under Contents/MacOS. Skip it for local dev builds
# and let the linker ad-hoc sign; the shipped bundle is signed by the
# Ninja/CI packaging path (build_release_macos.sh, build_orca.yml).
set_target_properties(OrcaSlicer PROPERTIES XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED NO)
endif()
endif()
add_custom_command(TARGET OrcaSlicer POST_BUILD
COMMAND ln -sfn "${SLIC3R_RESOURCES_DIR}" "${BIN_RESOURCES_DIR}"
COMMENT "Symlinking the resources directory into the build tree"
VERBATIM)
add_custom_command(TARGET OrcaSlicer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -rf "${BIN_DIR}/python"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_PREFIX_PATH}/libpython" "${BIN_DIR}/python"
COMMENT "Copying Python runtime into the build tree"
VERBATIM)
# Stage uv next to the binary for the build-tree/.app flows, which never
# run install() -- see the layout map above; lookup is bundled_uv_path().
if(ORCA_BUNDLED_UV_EXECUTABLE AND EXISTS "${ORCA_BUNDLED_UV_EXECUTABLE}")
add_custom_command(TARGET OrcaSlicer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${BIN_DIR}/tools/uv"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${ORCA_BUNDLED_UV_EXECUTABLE}" "${BIN_DIR}/tools/uv/${ORCA_BUNDLED_UV_FILENAME}"
COMMAND chmod +x "${BIN_DIR}/tools/uv/${ORCA_BUNDLED_UV_FILENAME}"
COMMENT "Copying bundled uv into the build tree"
VERBATIM)
endif()
endif ()
# Slic3r binary install target. Default build type is release in case no CMAKE_BUILD_TYPE is provided.
@@ -288,6 +357,7 @@ if (WIN32)
install(TARGETS OrcaSlicer_app_gui RUNTIME DESTINATION ".")
endif ()
install(FILES ${output_dlls_${build_type}} DESTINATION ".")
install(DIRECTORY "${CMAKE_PREFIX_PATH}/libpython/" DESTINATION "python")
else ()
install(TARGETS OrcaSlicer RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR})
endif ()
+125 -11
View File
@@ -24,6 +24,8 @@
#include <iostream>
#include <math.h>
#include <csignal>
#include <atomic>
#include <new>
#if defined(__linux__) || defined(__LINUX__)
#include <condition_variable>
@@ -51,7 +53,6 @@ using namespace nlohmann;
#include "libslic3r/Config.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/GCode.hpp"
#include "libslic3r/GCode/PostProcessor.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/ModelArrange.hpp"
#include "libslic3r/Platform.hpp"
@@ -1317,9 +1318,23 @@ int CLI::run(int argc, char **argv)
return CLI_INVALID_PARAMS;
}
BOOST_LOG_TRIVIAL(info) << "finished setup params, argc="<< argc << std::endl;
std::string temp_path = wxFileName::GetTempDir().utf8_str().data();
std::string temp_path = per_user_temp_dir(wxFileName::GetTempDir().utf8_str().data(), per_user_temp_id());
// Some consumers write into the temp root directly, so create it up front.
try {
boost::filesystem::create_directories(temp_path);
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(warning) << "failed to create per-user temp dir " << temp_path << ": " << ex.what();
}
set_temporary_dir(temp_path);
// The Filament Track Switch flags are live-device state with no meaning in headless slicing;
// default both off unless explicitly provided on the command line, so an old 3MF that had
// them enabled still slices without the switch behavior.
if (!m_extra_config.has("has_filament_switcher"))
m_extra_config.set_key_value("has_filament_switcher", new ConfigOptionBool(false));
if (!m_extra_config.has("enable_filament_dynamic_map"))
m_extra_config.set_key_value("enable_filament_dynamic_map", new ConfigOptionBool(false));
m_extra_config.apply(m_config, true);
m_extra_config.normalize_fdm();
@@ -1697,11 +1712,13 @@ int CLI::run(int argc, char **argv)
const Vec3d &instance_offset = model_instance->get_offset();
BOOST_LOG_TRIVIAL(info) << boost::format("instance %1% transform {%2%,%3%,%4%} at %5%:%6%")% model_object->name % instance_offset.x() % instance_offset.y() %instance_offset.z() % __FUNCTION__ % __LINE__<< std::endl;
}*/
current_printer_name = config.option<ConfigOptionString>("printer_settings_id")->value;
current_process_name = config.option<ConfigOptionString>("print_settings_id")->value;
// Read defensively — a 3mf missing preset ids (e.g. one produced
// by a non-GUI writer) would otherwise crash here on the deref.
if (const auto *o = config.option<ConfigOptionString>("printer_settings_id")) current_printer_name = o->value;
if (const auto *o = config.option<ConfigOptionString>("print_settings_id")) current_process_name = o->value;
current_printer_model = config.option<ConfigOptionString>("printer_model", true)->value;
current_filaments_name = config.option<ConfigOptionStrings>("filament_settings_id")->values;
current_extruder_count = config.option<ConfigOptionFloats>("nozzle_diameter")->values.size();
if (const auto *o = config.option<ConfigOptionStrings>("filament_settings_id")) current_filaments_name = o->values;
if (const auto *o = config.option<ConfigOptionFloats>("nozzle_diameter")) current_extruder_count = o->values.size();
current_printer_variant_count = config.option<ConfigOptionStrings>("printer_extruder_variant", true)->values.size();
current_print_variant_count = config.option<ConfigOptionStrings>("print_extruder_variant", true)->values.size();
current_is_multi_extruder = current_extruder_count > 1;
@@ -5931,6 +5948,72 @@ int CLI::run(int argc, char **argv)
else
filament_maps = part_plate->get_real_filament_maps(m_print_config);
// Multi-nozzle printers need the per-filament volume assignment as a grouping
// input in the manual modes: synthesize it from the per-extruder flow types when
// the caller did not provide one (an extruder whose nozzle stats span several
// volume types keeps the per-filament choice), and require explicit maps in
// nozzle-manual mode.
auto max_nozzle_counts_opt = m_print_config.option<ConfigOptionIntsNullable>("extruder_max_nozzle_count");
// Skip nil entries: a nullable-int nil is INT_MAX (> 1) and would otherwise falsely pass the gate.
bool support_multi_nozzle =
max_nozzle_counts_opt &&
std::any_of(max_nozzle_counts_opt->values.begin(), max_nozzle_counts_opt->values.end(),
[](int v) { return v > 1 && v != ConfigOptionIntsNullable::nil_value(); });
if (support_multi_nozzle && (mode == fmmManual || mode == fmmNozzleManual) && (plate_to_slice != 0)) {
// Orca: the grouping result is reconstructed purely from the passed maps in
// nozzle-manual mode, so all of them must be present (there are no separate
// per-nozzle CLI parameters to rebuild them from).
if (mode == FilamentMapMode::fmmNozzleManual &&
(!m_extra_config.has("filament_volume_map") || !m_extra_config.has("filament_nozzle_map") ||
!m_extra_config.has("filament_map"))) {
BOOST_LOG_TRIVIAL(error)
<< boost::format("%1%, can not find filament_volume_map/filament_nozzle_map/filament_map under Nozzle Manual mode") % __LINE__;
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, index + 1, cli_errors[CLI_INVALID_PARAMS], sliced_info);
flush_and_exit(CLI_INVALID_PARAMS);
}
if (mode == fmmManual) {
// Build the volume map when absent: filaments on a single-volume extruder
// print with that extruder's volume; a mixed-volume extruder keeps the
// per-filament choice (default Standard).
std::vector<NozzleVolumeType> using_nozzle_volume_type = new_nozzle_volume_type;
using_nozzle_volume_type.resize(new_extruder_count, nvtStandard);
if (auto extruder_nozzle_stats_opt = m_print_config.option<ConfigOptionStrings>("extruder_nozzle_stats")) {
auto nozzle_stats = get_extruder_nozzle_stats(extruder_nozzle_stats_opt->values);
for (int e_index = 0; e_index < new_extruder_count && e_index < (int) nozzle_stats.size(); e_index++) {
if (nozzle_stats[e_index].size() > 1) {
using_nozzle_volume_type[e_index] = nvtHybrid;
BOOST_LOG_TRIVIAL(info) << boost::format("%1% : extruder %2%, set nozzle_volume_type to hybrid ") % __LINE__ % (e_index + 1);
}
}
}
std::vector<int> &manual_volume_maps = m_extra_config.option<ConfigOptionInts>("filament_volume_map", true)->values;
// default to standard flow
manual_volume_maps.resize(filament_count, (int) (nvtStandard));
for (int f_index = 0; f_index < filament_count && f_index < (int) filament_maps.size(); f_index++) {
int f_extruder_index = filament_maps[f_index] - 1;
if (f_extruder_index >= 0 && f_extruder_index < new_extruder_count &&
using_nozzle_volume_type[f_extruder_index] != nvtHybrid) {
manual_volume_maps[f_index] = int(using_nozzle_volume_type[f_extruder_index]);
BOOST_LOG_TRIVIAL(info) << boost::format("%1% : filament %2% extruder %3%, set filament_volume_map to %4% ") % __LINE__ % (f_index + 1) % (f_extruder_index + 1) % manual_volume_maps[f_index];
}
}
}
if (m_extra_config.has("filament_volume_map")) {
part_plate->set_filament_volume_maps(m_extra_config.option<ConfigOptionInts>("filament_volume_map")->values);
}
if (m_extra_config.has("filament_nozzle_map")) {
part_plate->set_filament_nozzle_maps(m_extra_config.option<ConfigOptionInts>("filament_nozzle_map")->values);
}
}
else if (!support_multi_nozzle && (mode == fmmNozzleManual)) {
BOOST_LOG_TRIVIAL(error)
<< boost::format("%1%, Nozzle Manual mode not supported for %2%") % __LINE__ % new_printer_name;
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, index + 1, cli_errors[CLI_INVALID_PARAMS], sliced_info);
flush_and_exit(CLI_INVALID_PARAMS);
}
for (int index = 0; index < filament_maps.size(); index++)
{
int filament_extruder = filament_maps[index];
@@ -6169,6 +6252,22 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << "print::process: first time_using_cache is " << time_using_cache << " secs.";
}
if (printer_technology == ptFFF) {
// Read the engine's final grouping back onto the plate so an exported
// project (--export-3mf / gcode.3mf) carries the concrete maps in its
// plate settings, matching what a GUI slice persists.
// Orca: deliberately gated to multi-extruder printers so single-extruder
// exports keep their plate settings unchanged.
if (new_extruder_count > 1) {
FilamentMapMode current_map_mode = print_fff->config().filament_map_mode.value;
if (is_auto_filament_map_mode(current_map_mode)) {
part_plate->set_filament_maps(print_fff->get_filament_maps());
part_plate->set_filament_volume_maps(print_fff->get_filament_volume_maps());
}
if (current_map_mode != FilamentMapMode::fmmNozzleManual) {
part_plate->set_filament_nozzle_maps(print_fff->get_filament_nozzle_maps());
}
}
std::string conflict_result = print_fff->get_conflict_string();
if (!conflict_result.empty()) {
BOOST_LOG_TRIVIAL(error) << "plate "<< index+1<< ": found slicing result conflict!"<< std::endl;
@@ -6955,7 +7054,7 @@ int CLI::run(int argc, char **argv)
gcode_viewer.render_calibration_thumbnail(*calibration_data, cali_thumbnail_width, cali_thumbnail_height,
calibration_params, partplate_list, opengl_mgr);
//generate_calibration_thumbnail(*calibration_data, thumbnail_width, thumbnail_height, calibration_params);
//*plate_bboxes[index] = p->generate_first_layer_bbox();
// *plate_bboxes[index] = p->generate_first_layer_bbox();
calibration_thumbnails.push_back(calibration_data);*/
PlateBBoxData* plate_bbox = new PlateBBoxData();
@@ -7353,7 +7452,7 @@ bool CLI::export_project(Model *model, std::string& path, PlateDataPtrs &partpla
bool success = false;
StoreParams store_params;
store_params.path = path.c_str();
store_params.path = path;
store_params.model = model;
store_params.plate_data_list = partplate_data;
store_params.project_presets = project_presets;
@@ -7511,6 +7610,9 @@ LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
}*/
#if defined(_MSC_VER) || defined(__MINGW32__)
// Guards against a failed allocation inside the dump re-entering the new-handler.
static std::atomic<bool> g_dump_in_progress{false};
extern "C" {
__declspec(dllexport) int __stdcall orcaslicer_main(int argc, wchar_t **argv)
{
@@ -7529,10 +7631,22 @@ extern "C" {
//AddVectoredExceptionHandler(1, CBaseException::UnhandledExceptionFilter);
SET_DEFULTER_HANDLER();
#endif
// Dump before unwinding, while the stack still names what asked for the memory. Throwing
// std::bad_alloc is standard-permitted here and is what reaches generic_exception_handle().
std::set_new_handler([]() {
int *a = nullptr;
*a = 0;
});
if (!g_dump_in_progress.exchange(true)) {
try {
// A null EXCEPTION_POINTERS walks the calling thread as it stands.
CBaseException base(GetCurrentProcess(), GetCurrentProcessId(), NULL, nullptr);
base.ShowCallstack();
} catch (...) {
// A failed dump must not displace the std::bad_alloc owed to the caller.
}
// ObjParser recovers from std::bad_alloc, so let a later one dump again.
g_dump_in_progress = false;
}
throw std::bad_alloc();
});
// Call the UTF8 main.
return CLI().run(argc, argv_ptrs.data());
}
+8
View File
@@ -263,6 +263,14 @@ int wmain(int argc, wchar_t **argv)
_wsplitpath(path_to_exe, drive, dir, fname, ext);
_wmakepath(path_to_exe, drive, dir, nullptr, nullptr);
wchar_t path_to_python[MAX_PATH + 1] = { 0 };
wcscpy(path_to_python, path_to_exe);
wcscat(path_to_python, L"python");
DWORD python_attrs = GetFileAttributesW(path_to_python);
if (python_attrs != INVALID_FILE_ATTRIBUTES && (python_attrs & FILE_ATTRIBUTE_DIRECTORY)) {
SetDllDirectoryW(path_to_python);
}
#ifdef SLIC3R_GUI
// https://wiki.qt.io/Cross_compiling_Mesa_for_Windows
// http://download.qt.io/development_releases/prebuilt/llvmpipe/windows/
+301 -3
View File
@@ -1,12 +1,32 @@
// This single-TU executable links libslic3r, whose SVG/emboss objects (pulled in by the slice mode
// below) reference the header-only nanosvg implementation. Provide it here BEFORE any libslic3r header:
// several of them transitively include nanosvg.h without the implementation macro, and its include
// guard would then suppress the implementation if the macro were defined afterwards. Same pattern as
// the test mains.
#define NANOSVG_IMPLEMENTATION
#include "nanosvg/nanosvg.h"
#define NANOSVGRAST_IMPLEMENTATION
#include "nanosvg/nanosvgrast.h"
#include "libslic3r/GCode.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/filesystem/operations.hpp>
#include <boost/log/trivial.hpp>
#include <boost/log/core.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/sinks/sync_frontend.hpp>
#include <boost/log/sinks/text_ostream_backend.hpp>
#include <boost/core/null_deleter.hpp>
#include <boost/make_shared.hpp>
#include <boost/program_options.hpp>
#include <algorithm>
#include <fstream>
#include <iostream>
#include <string>
@@ -83,6 +103,270 @@ void generate_custom_presets(PresetBundle* preset_bundle, AppConfig& app_config)
std::cout << "Custom presets generated successfully" << std::endl;
}
namespace {
Vec2d printable_area_center(const DynamicPrintConfig &cfg)
{
const auto *opt = cfg.option<ConfigOptionPoints>("printable_area");
if (opt == nullptr || opt->values.empty())
return Vec2d(100., 100.);
Vec2d lo = opt->values.front(), hi = opt->values.front();
for (const Vec2d &p : opt->values) { lo = lo.cwiseMin(p); hi = hi.cwiseMax(p); }
return 0.5 * (lo + hi);
}
// Slice one centered cube that switches from filament 1 to filament 2 partway up, so exactly one
// filament change fires, then export. The change drives the printer's own change_filament_gcode: on a
// single-nozzle machine it rides the AMS prime tower (append_tcr), on a multi-nozzle machine it routes
// through the nozzle swap (set_extruder / append_tcr2) - the engine picks the path from the printer's
// topology, so one model covers both. An undefined placeholder in any shipped custom g-code throws
// Slic3r::PlaceholderParserError from export.
std::string slice_two_color_cube_and_export(const DynamicPrintConfig &cfg, bool is_bbl)
{
const Vec2d center = printable_area_center(cfg);
TriangleMesh m = make_cube(10, 10, 10);
m.translate(float(center.x() - 5.), float(center.y() - 5.), 0.f);
Model model;
Print print;
ModelObject *obj = model.add_object();
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
obj->add_volume(m);
obj->add_instance();
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
DynamicPrintConfig range_config;
range_config.set_key_value("extruder", new ConfigOptionInt(2));
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
// trips Flow::with_spacing.
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
print.is_BBL_printer() = is_bbl;
obj->ensure_on_bed();
print.auto_assign_extruders(obj);
print.apply(model, cfg);
print.validate();
// Process + export to a temp file, then read it back (the app's own export path is where the
// custom *_gcode placeholders expand).
print.set_status_silent();
print.process();
const fs::path tmp = fs::temp_directory_path() / fs::unique_path("orca-validate-%%%%-%%%%.gcode");
print.export_gcode(tmp.string(), nullptr, nullptr);
std::ifstream in(tmp.string());
std::string out((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
in.close();
boost::system::error_code ec;
fs::remove(tmp, ec);
return out;
}
// Select the printer's OWN default process + filament (as the app does on a printer change) so we
// slice with settings the printer actually ships, not the generic "Default Setting" that stays
// selected because it is compatible with every printer.
void select_printer_default_presets(PresetBundle &bundle)
{
const Preset &printer_preset = bundle.printers.get_selected_preset();
const std::string def_print = printer_preset.config.opt_string("default_print_profile");
if (!def_print.empty())
bundle.prints.select_preset_by_name(def_print, /*force=*/true);
if (const auto *def_fil = printer_preset.config.option<ConfigOptionStrings>("default_filament_profile");
def_fil != nullptr && !def_fil->values.empty())
bundle.filaments.select_preset_by_name(def_fil->values.front(), /*force=*/true);
}
// The vendor/printer currently being sliced, stamped onto every engine log record by the sink below so
// the interleaved [error] lines can be attributed to a profile. Updated once per loop iteration; safe as
// a plain global because the sweep is single-threaded and synchronous (see slice_all_printers).
static std::string g_slice_context;
// Route Boost.Log through a sink that prefixes every record with g_slice_context. Without this the
// engine's [error] lines (emitted deep inside process()/export_gcode()) carry no printer context, so a
// failing profile cannot be told apart from the ~1000 others in the sweep. Drops the default trivial
// sink's timestamp/thread columns - noise here - in favour of the vendor/printer tag.
void install_slice_context_log_sink()
{
namespace logging = boost::log;
namespace sinks = boost::log::sinks;
namespace expr = boost::log::expressions;
auto backend = boost::make_shared<sinks::text_ostream_backend>();
backend->add_stream(boost::shared_ptr<std::ostream>(&std::clog, boost::null_deleter()));
backend->auto_flush(true);
auto sink = boost::make_shared<sinks::synchronous_sink<sinks::text_ostream_backend>>(backend);
sink->set_formatter([](const logging::record_view &rec, logging::formatting_ostream &strm) {
strm << "[" << rec[logging::trivial::severity] << "]";
if (!g_slice_context.empty())
strm << " [" << g_slice_context << "]";
strm << " " << rec[expr::smessage];
});
logging::core::get()->remove_all_sinks(); // drop the default trivial sink so lines are not doubled
logging::core::get()->add_sink(sink);
}
// Slice-and-export a two-colour cube through every shipped printer (optionally scoped to one vendor via
// -v). Unlike the static reference/placeholder checks, this expands every custom *_gcode - including
// change_filament_gcode at the one filament change - against the printer's fully-resolved config, so
// undefined-placeholder / invalid-flow bugs surface here. Reports every offending printer and returns 1
// if any failed, 0 otherwise. When outdir is non-empty, each printer's g-code is also written there as
// "<vendor>__<printer>.gcode" for manual inspection. The sweep is SEQUENTIAL by necessity:
// Print::process() keeps process-global state, so slicing printers concurrently in one process races
// even with per-slice Model+Print. Load in validation mode so the vendors are read straight from the -p
// profiles dir (no data_dir/system tree) and -v scoping is honoured for free.
int slice_all_printers(const std::string &vendor, const std::string &outdir)
{
install_slice_context_log_sink();
if (!outdir.empty()) {
boost::system::error_code ec;
fs::create_directories(outdir, ec);
if (ec) {
BOOST_LOG_TRIVIAL(error) << "Could not create output directory \"" << outdir << "\": " << ec.message();
std::cout << "Validation failed" << std::endl;
return 1;
}
std::cout << "Saving sliced g-code to " << outdir << std::endl;
}
PresetBundle bundle;
bundle.set_is_validation_mode(true);
bundle.set_vendor_to_validate(vendor); // empty == all vendors
AppConfig app_config;
app_config.set("preset_folder", "default");
try {
bundle.load_presets(app_config, ForwardCompatibilitySubstitutionRule::Disable);
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << ex.what();
std::cout << "Validation failed" << std::endl;
return 1;
}
// Enable every instantiable model/variant in AppConfig - system printers are hidden until enabled;
// without this select_preset_by_name silently falls back to the "Default Printer".
std::vector<std::pair<std::string, std::string>> printers; // (vendor name, preset name)
for (const Preset &p : bundle.printers.get_presets()) {
if (p.vendor == nullptr) continue; // skips the Default Printer
const std::string model = p.config.opt_string("printer_model");
const std::string variant = p.config.opt_string("printer_variant");
if (model.empty() || variant.empty()) continue; // skip non-instantiable base/common configs
app_config.set_variant(p.vendor->id, model, variant, true);
printers.push_back({p.vendor->name, p.name});
}
bundle.load_installed_printers(app_config);
if (printers.empty()) {
BOOST_LOG_TRIVIAL(error) << "No instantiable printer presets found"
<< (vendor.empty() ? "" : " for vendor " + vendor);
std::cout << "Validation failed" << std::endl;
return 1;
}
std::cout << "Slicing " << printers.size() << " printer preset(s)"
<< (vendor.empty() ? "" : " for vendor " + vendor) << "..." << std::endl;
int failures = 0;
for (const auto &[vendor_name, printer] : printers) {
g_slice_context = vendor_name + " / " + printer; // tag every engine log line from this slice
const bool selected = bundle.printers.select_preset_by_name(printer, /*force=*/true);
if (!selected || bundle.printers.get_selected_preset_name() != printer) {
BOOST_LOG_TRIVIAL(error) << "Printer preset \"" << printer << "\" could not be selected";
++failures;
continue;
}
select_printer_default_presets(bundle); // slice with the printer's shipped process/filament
bundle.update_multi_material_filament_presets(); // size filament_presets to nozzle count
bundle.update_compatible(PresetSelectCompatibleType::Always);
// Never slice with a generic default preset - that would validate stand-in settings, not the
// real profile (a legit per-profile error).
if (bundle.prints.get_selected_preset().is_default || bundle.filaments.get_selected_preset().is_default) {
BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer << "\" fell back to a default preset (process=\""
<< bundle.prints.get_selected_preset_name() << "\", filament=\""
<< bundle.filaments.get_selected_preset_name() << "\")";
++failures;
continue;
}
// Grow to a 2nd filament so the cube can change colour; never shrink a multi-nozzle printer
// below its nozzle count, or full_config()'s flush-volume matrix no longer matches validate().
const size_t nozzles = bundle.printers.get_selected_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->size();
bundle.set_num_filaments((unsigned int) std::max<size_t>(2, nozzles));
// Mirror the app's manual filament->nozzle assignment for a multi-nozzle BBL printer: put each
// filament on its own nozzle and pin the map (fmmManual) so full_config() collapses every filament to
// the variant of the nozzle it actually prints from, and the engine keeps that assignment instead of
// auto-remapping it during process(). Without this the synthetic 2nd filament keeps nozzle 1's variant
// while the auto map moves it to nozzle 2 - harmless, but on the one printer whose nozzles differ in
// type (Direct Drive + Bowden) the mismatched lookup spams [error] lines. Single-nozzle and non-BBL
// printers keep the default map (their toolchange rides the AMS/tool-changer path unchanged).
const bool pin_filament_map = bundle.is_bbl_vendor() && nozzles > 1;
if (pin_filament_map) {
auto &fmap = bundle.project_config.option<ConfigOptionInts>("filament_map", true)->values;
for (size_t i = 0; i < fmap.size(); ++i)
fmap[i] = int(i % nozzles) + 1;
}
DynamicPrintConfig cfg = bundle.full_config();
cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); // force a purge tower so the change is detectable
// The map above drives full_config()'s per-filament variant collapse; fmmManual on the sliced config
// stops process() from auto-remapping filaments back onto a different nozzle (which would re-introduce
// the variant mismatch this pinning avoids).
if (pin_filament_map)
cfg.set_key_value("filament_map_mode", new ConfigOptionEnum<FilamentMapMode>(fmmManual));
// full_config() grows filament_extruder_variant to one entry per filament, but because the synthetic
// 2nd filament is a duplicate of the first (set_num_filaments copies the same preset), it leaves
// filament_self_index at size 1. That makes update_values_to_printer_extruders_for_multiple_filaments
// fail to resolve the 2nd filament's variant - a benign fallback that spams [error] lines. A real
// 2-colour project ships filament_self_index = 1,2,...; mirror that so the sweep log stays clean. The
// slice output is unaffected: the duplicated filament's per-variant values are identical to the first.
if (auto *variants = cfg.option<ConfigOptionStrings>("filament_extruder_variant")) {
auto &self_index = cfg.option<ConfigOptionInts>("filament_self_index", true)->values;
if (self_index.size() != variants->size()) {
self_index.resize(variants->size());
for (size_t i = 0; i < self_index.size(); ++i)
self_index[i] = int(i) + 1;
}
}
try {
const std::string out = slice_two_color_cube_and_export(cfg, bundle.is_bbl_vendor());
if (!outdir.empty() && !out.empty()) {
const fs::path f = fs::path(outdir) / (sanitize_filename(vendor_name) + "__" + sanitize_filename(printer) + ".gcode");
save_string_file(f, out);
}
if (out.empty() || out.find("G1") == std::string::npos) {
BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer << "\" produced no g-code";
++failures;
} else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
// The filament change never rode the tower, so change_filament_gcode was not exercised.
BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer
<< "\" sliced but the filament change never fired (no CP TOOLCHANGE START)";
++failures;
}
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer << "\" failed to slice: " << ex.what();
++failures;
}
}
g_slice_context.clear();
if (failures > 0) {
std::cout << failures << " of " << printers.size() << " printer preset(s) failed to slice" << std::endl;
std::cout << "Validation failed" << std::endl;
return 1;
}
std::cout << "All " << printers.size() << " printer preset(s) sliced successfully" << std::endl;
std::cout << "Validation completed successfully" << std::endl;
return 0;
}
} // namespace
int main(int argc, char* argv[])
{
po::options_description desc("Orca Profile Validator\nUsage");
@@ -95,8 +379,9 @@ int main(int argc, char* argv[])
#endif
("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional, all profiles present in the folder will be validated if not specified")
("generate_presets,g", po::value<bool>()->default_value(false), "Generate user presets for mock test")
("slice,s", po::bool_switch()->default_value(false), "Slice a two-colour cube through every printer to expand all custom g-code (catches placeholder/flow errors that static checks miss). Off unless this flag is present.")
("outdir,o", po::value<std::string>()->default_value(""), "With -s, also save each printer's g-code to this folder (as <vendor>__<printer>.gcode) for manual inspection. Optional.")
("check_filament_subtypes,f", po::bool_switch()->default_value(false), "Also flag printers with duplicate (ambiguous) filament subtypes. Off unless this flag is present.")
("check_preset_references,r", po::bool_switch()->default_value(false), "Also flag presets whose inherits/compatible_printers/compatible_prints reference a deleted or renamed preset. Off unless this flag is present.")
("log_level,l", po::value<int>()->default_value(2), "Log level. Optional, default is 2 (warning). Higher values produce more detailed logs.");
// clang-format on
@@ -120,8 +405,9 @@ int main(int argc, char* argv[])
std::string vendor = vm["vendor"].as<std::string>();
int log_level = vm["log_level"].as<int>();
bool generate_user_preset = vm["generate_presets"].as<bool>();
bool slice_mode = vm["slice"].as<bool>();
std::string slice_outdir = vm["outdir"].as<std::string>();
bool check_filament_subtypes = vm["check_filament_subtypes"].as<bool>();
bool check_preset_references = vm["check_preset_references"].as<bool>();
// check if path is valid, and return error if not
if (!fs::exists(path) || !fs::is_directory(path)) {
@@ -134,6 +420,12 @@ int main(int argc, char* argv[])
// std::cout<<"log_level: "<<log_level<<std::endl;
set_data_dir(path);
// Orca: the profiles folder lives at <resources>/profiles, so point resources_dir() at that
// <resources> parent. Without this, resources_dir() is empty and slice mode's HRC lookup
// (info/nozzle_info.json) resolves to a non-existent relative path and falls back to a
// built-in table (logging a spurious parse error and dropping the E3D entry).
if (fs::exists(fs::path(path).parent_path() / "info"))
set_resources_dir(fs::path(path).parent_path().string());
auto user_dir = fs::path(Slic3r::data_dir()) / PRESET_USER_DIR;
user_dir.make_preferred();
@@ -141,6 +433,12 @@ int main(int argc, char* argv[])
fs::create_directory(user_dir);
set_logging_level(log_level);
// Slice mode expands every printer's custom g-code by actually slicing (see slice_all_printers).
// A distinct opt-in mode so the default static checks stay fast for every profile PR.
if (slice_mode)
return slice_all_printers(vendor, slice_outdir);
auto preset_bundle = new PresetBundle();
// preset_bundle->setup_directories();
preset_bundle->set_is_validation_mode(true);
@@ -168,7 +466,7 @@ int main(int argc, char* argv[])
return 0;
}
if (preset_bundle->has_errors(check_filament_subtypes, check_preset_references)) {
if (preset_bundle->has_errors(check_filament_subtypes)) {
std::cout << "Validation failed" << std::endl;
return 1;
}
@@ -171,6 +171,33 @@ echo -n "[9/9] Generating Linux app..."
fi
cp -fl "${ORIGINAL_BINARY_LOCATION}" "$BIN_DIR/@SLIC3R_APP_CMD@"
# Bundle the embedded Python runtime (interpreter + stdlib) that CMake staged
# next to the binary. OrcaSlicer resolves PYTHONHOME at <resources_dir>/../lib/python,
# which is $APPDIR/lib/python (resources_dir is $APPDIR/resources). Staging it here,
# before the dependency-closure pass below, lets that pass also bundle the extension
# modules' shared libraries; libpython itself is found via rpath ($ORIGIN entries set
# in src/CMakeLists.txt and by the dep recipe). Without this the plugin system cannot start.
PYTHON_RUNTIME_SRC="$(dirname "${ORIGINAL_BINARY_LOCATION}")/python"
if [ -d "$PYTHON_RUNTIME_SRC" ]; then
echo "Bundling Python runtime from ${PYTHON_RUNTIME_SRC} ..."
copy_directory_if_present "$PYTHON_RUNTIME_SRC" "$LIB_DIR/python"
else
echo "Warning: bundled Python runtime not found at ${PYTHON_RUNTIME_SRC}; plugin support will be disabled in this AppImage."
fi
# Bundle the uv executable (CMake staged it next to the binary) so the plugin
# system can install Python package dependencies. bundled_uv_path() looks under
# <resources_dir>/tools/uv == $APPDIR/resources/tools/uv -- the same layout the
# install()-based packagers (Windows/Flatpak/FHS) use. Unlike the AppImage, those
# run `make install`; this build copies artifacts, so stage uv explicitly.
UV_RUNTIME_SRC="$(dirname "${ORIGINAL_BINARY_LOCATION}")/tools/uv"
if [ -d "$UV_RUNTIME_SRC" ]; then
echo "Bundling uv from ${UV_RUNTIME_SRC} ..."
copy_directory_if_present "$UV_RUNTIME_SRC" "$APPDIR/resources/tools/uv"
else
echo "Warning: bundled uv not found at ${UV_RUNTIME_SRC}; Python package installation will be unavailable in this AppImage."
fi
if [ "$BUNDLE_DESKTOP_STACK" = "1" ]; then
GSTREAMER_PLUGIN_SOURCE_DIR="$(find_pkg_config_dir pluginsdir gstreamer-1.0 || true)"
if [ -z "$GSTREAMER_PLUGIN_SOURCE_DIR" ]; then
+21 -4
View File
@@ -1,11 +1,11 @@
/**
* Loader generated by glad 2.0.8 on Tue Apr 7 00:45:10 2026
* Loader generated by glad 2.0.8 on Thu Jul 16 03:42:05 2026
*
* SPDX-License-Identifier: (WTFPL OR CC0-1.0) AND Apache-2.0
*
* Generator: C/C++
* Specification: gl
* Extensions: 5
* Extensions: 7
*
* APIs:
* - gl:compatibility=4.6
@@ -19,10 +19,10 @@
* - ON_DEMAND = False
*
* Commandline:
* --api='gl:compatibility=4.6' --extensions='GL_ARB_compatibility,GL_ARB_framebuffer_object,GL_EXT_framebuffer_object,GL_EXT_texture_compression_s3tc,GL_EXT_texture_filter_anisotropic' c --loader
* --api='gl:compatibility=4.6' --extensions='GL_ARB_compatibility,GL_ARB_framebuffer_object,GL_EXT_framebuffer_blit,GL_EXT_framebuffer_multisample,GL_EXT_framebuffer_object,GL_EXT_texture_compression_s3tc,GL_EXT_texture_filter_anisotropic' c --loader
*
* Online:
* http://glad.sh/#api=gl%3Acompatibility%3D4.6&extensions=GL_ARB_compatibility%2CGL_ARB_framebuffer_object%2CGL_EXT_framebuffer_object%2CGL_EXT_texture_compression_s3tc%2CGL_EXT_texture_filter_anisotropic&generator=c&options=LOADER
* http://glad.sh/#api=gl%3Acompatibility%3D4.6&extensions=GL_ARB_compatibility%2CGL_ARB_framebuffer_object%2CGL_EXT_framebuffer_blit%2CGL_EXT_framebuffer_multisample%2CGL_EXT_framebuffer_object%2CGL_EXT_texture_compression_s3tc%2CGL_EXT_texture_filter_anisotropic&generator=c&options=LOADER
*
*/
@@ -616,6 +616,8 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_DRAW_BUFFER9 0x882E
#define GL_DRAW_FRAMEBUFFER 0x8CA9
#define GL_DRAW_FRAMEBUFFER_BINDING 0x8CA6
#define GL_DRAW_FRAMEBUFFER_BINDING_EXT 0x8CA6
#define GL_DRAW_FRAMEBUFFER_EXT 0x8CA9
#define GL_DRAW_INDIRECT_BUFFER 0x8F3F
#define GL_DRAW_INDIRECT_BUFFER_BINDING 0x8F43
#define GL_DRAW_PIXEL_TOKEN 0x0705
@@ -746,6 +748,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT 0x8CD7
#define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT 0x8CD7
#define GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE 0x8D56
#define GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE_EXT 0x8D56
#define GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER 0x8CDC
#define GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT 0x8CDC
#define GL_FRAMEBUFFER_RENDERABLE 0x8289
@@ -1106,6 +1109,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_MAX_RENDERBUFFER_SIZE 0x84E8
#define GL_MAX_RENDERBUFFER_SIZE_EXT 0x84E8
#define GL_MAX_SAMPLES 0x8D57
#define GL_MAX_SAMPLES_EXT 0x8D57
#define GL_MAX_SAMPLE_MASK_WORDS 0x8E59
#define GL_MAX_SERVER_WAIT_TIMEOUT 0x9111
#define GL_MAX_SHADER_STORAGE_BLOCK_SIZE 0x90DE
@@ -1400,6 +1404,8 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_READ_BUFFER 0x0C02
#define GL_READ_FRAMEBUFFER 0x8CA8
#define GL_READ_FRAMEBUFFER_BINDING 0x8CAA
#define GL_READ_FRAMEBUFFER_BINDING_EXT 0x8CAA
#define GL_READ_FRAMEBUFFER_EXT 0x8CA8
#define GL_READ_ONLY 0x88B8
#define GL_READ_PIXELS 0x828C
#define GL_READ_PIXELS_FORMAT 0x828D
@@ -1437,6 +1443,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_RENDERBUFFER_RED_SIZE 0x8D50
#define GL_RENDERBUFFER_RED_SIZE_EXT 0x8D50
#define GL_RENDERBUFFER_SAMPLES 0x8CAB
#define GL_RENDERBUFFER_SAMPLES_EXT 0x8CAB
#define GL_RENDERBUFFER_STENCIL_SIZE 0x8D55
#define GL_RENDERBUFFER_STENCIL_SIZE_EXT 0x8D55
#define GL_RENDERBUFFER_WIDTH 0x8D42
@@ -2150,6 +2157,10 @@ GLAD_API_CALL int GLAD_GL_VERSION_4_6;
GLAD_API_CALL int GLAD_GL_ARB_compatibility;
#define GL_ARB_framebuffer_object 1
GLAD_API_CALL int GLAD_GL_ARB_framebuffer_object;
#define GL_EXT_framebuffer_blit 1
GLAD_API_CALL int GLAD_GL_EXT_framebuffer_blit;
#define GL_EXT_framebuffer_multisample 1
GLAD_API_CALL int GLAD_GL_EXT_framebuffer_multisample;
#define GL_EXT_framebuffer_object 1
GLAD_API_CALL int GLAD_GL_EXT_framebuffer_object;
#define GL_EXT_texture_compression_s3tc 1
@@ -2205,6 +2216,7 @@ typedef void (GLAD_API_PTR *PFNGLBLENDFUNCSEPARATEPROC)(GLenum sfactorRGB, GLenu
typedef void (GLAD_API_PTR *PFNGLBLENDFUNCSEPARATEIPROC)(GLuint buf, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha);
typedef void (GLAD_API_PTR *PFNGLBLENDFUNCIPROC)(GLuint buf, GLenum src, GLenum dst);
typedef void (GLAD_API_PTR *PFNGLBLITFRAMEBUFFERPROC)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
typedef void (GLAD_API_PTR *PFNGLBLITFRAMEBUFFEREXTPROC)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
typedef void (GLAD_API_PTR *PFNGLBLITNAMEDFRAMEBUFFERPROC)(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
typedef void (GLAD_API_PTR *PFNGLBUFFERDATAPROC)(GLenum target, GLsizeiptr size, const void * data, GLenum usage);
typedef void (GLAD_API_PTR *PFNGLBUFFERSTORAGEPROC)(GLenum target, GLsizeiptr size, const void * data, GLbitfield flags);
@@ -2881,6 +2893,7 @@ typedef GLint (GLAD_API_PTR *PFNGLRENDERMODEPROC)(GLenum mode);
typedef void (GLAD_API_PTR *PFNGLRENDERBUFFERSTORAGEPROC)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
typedef void (GLAD_API_PTR *PFNGLRENDERBUFFERSTORAGEEXTPROC)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
typedef void (GLAD_API_PTR *PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height);
typedef void (GLAD_API_PTR *PFNGLRENDERBUFFERSTORAGEMULTISAMPLEEXTPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height);
typedef void (GLAD_API_PTR *PFNGLRESUMETRANSFORMFEEDBACKPROC)(void);
typedef void (GLAD_API_PTR *PFNGLROTATEDPROC)(GLdouble angle, GLdouble x, GLdouble y, GLdouble z);
typedef void (GLAD_API_PTR *PFNGLROTATEFPROC)(GLfloat angle, GLfloat x, GLfloat y, GLfloat z);
@@ -3318,6 +3331,8 @@ GLAD_API_CALL PFNGLBLENDFUNCIPROC glad_glBlendFunci;
#define glBlendFunci glad_glBlendFunci
GLAD_API_CALL PFNGLBLITFRAMEBUFFERPROC glad_glBlitFramebuffer;
#define glBlitFramebuffer glad_glBlitFramebuffer
GLAD_API_CALL PFNGLBLITFRAMEBUFFEREXTPROC glad_glBlitFramebufferEXT;
#define glBlitFramebufferEXT glad_glBlitFramebufferEXT
GLAD_API_CALL PFNGLBLITNAMEDFRAMEBUFFERPROC glad_glBlitNamedFramebuffer;
#define glBlitNamedFramebuffer glad_glBlitNamedFramebuffer
GLAD_API_CALL PFNGLBUFFERDATAPROC glad_glBufferData;
@@ -4670,6 +4685,8 @@ GLAD_API_CALL PFNGLRENDERBUFFERSTORAGEEXTPROC glad_glRenderbufferStorageEXT;
#define glRenderbufferStorageEXT glad_glRenderbufferStorageEXT
GLAD_API_CALL PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC glad_glRenderbufferStorageMultisample;
#define glRenderbufferStorageMultisample glad_glRenderbufferStorageMultisample
GLAD_API_CALL PFNGLRENDERBUFFERSTORAGEMULTISAMPLEEXTPROC glad_glRenderbufferStorageMultisampleEXT;
#define glRenderbufferStorageMultisampleEXT glad_glRenderbufferStorageMultisampleEXT
GLAD_API_CALL PFNGLRESUMETRANSFORMFEEDBACKPROC glad_glResumeTransformFeedback;
#define glResumeTransformFeedback glad_glResumeTransformFeedback
GLAD_API_CALL PFNGLROTATEDPROC glad_glRotated;
+22 -1
View File
@@ -44,6 +44,8 @@ int GLAD_GL_VERSION_4_5 = 0;
int GLAD_GL_VERSION_4_6 = 0;
int GLAD_GL_ARB_compatibility = 0;
int GLAD_GL_ARB_framebuffer_object = 0;
int GLAD_GL_EXT_framebuffer_blit = 0;
int GLAD_GL_EXT_framebuffer_multisample = 0;
int GLAD_GL_EXT_framebuffer_object = 0;
int GLAD_GL_EXT_texture_compression_s3tc = 0;
int GLAD_GL_EXT_texture_filter_anisotropic = 0;
@@ -97,6 +99,7 @@ PFNGLBLENDFUNCSEPARATEPROC glad_glBlendFuncSeparate = NULL;
PFNGLBLENDFUNCSEPARATEIPROC glad_glBlendFuncSeparatei = NULL;
PFNGLBLENDFUNCIPROC glad_glBlendFunci = NULL;
PFNGLBLITFRAMEBUFFERPROC glad_glBlitFramebuffer = NULL;
PFNGLBLITFRAMEBUFFEREXTPROC glad_glBlitFramebufferEXT = NULL;
PFNGLBLITNAMEDFRAMEBUFFERPROC glad_glBlitNamedFramebuffer = NULL;
PFNGLBUFFERDATAPROC glad_glBufferData = NULL;
PFNGLBUFFERSTORAGEPROC glad_glBufferStorage = NULL;
@@ -773,6 +776,7 @@ PFNGLRENDERMODEPROC glad_glRenderMode = NULL;
PFNGLRENDERBUFFERSTORAGEPROC glad_glRenderbufferStorage = NULL;
PFNGLRENDERBUFFERSTORAGEEXTPROC glad_glRenderbufferStorageEXT = NULL;
PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC glad_glRenderbufferStorageMultisample = NULL;
PFNGLRENDERBUFFERSTORAGEMULTISAMPLEEXTPROC glad_glRenderbufferStorageMultisampleEXT = NULL;
PFNGLRESUMETRANSFORMFEEDBACKPROC glad_glResumeTransformFeedback = NULL;
PFNGLROTATEDPROC glad_glRotated = NULL;
PFNGLROTATEFPROC glad_glRotatef = NULL;
@@ -2249,6 +2253,14 @@ static void glad_gl_load_GL_ARB_framebuffer_object( GLADuserptrloadfunc load, vo
glad_glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC) load(userptr, "glRenderbufferStorage");
glad_glRenderbufferStorageMultisample = (PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC) load(userptr, "glRenderbufferStorageMultisample");
}
static void glad_gl_load_GL_EXT_framebuffer_blit( GLADuserptrloadfunc load, void* userptr) {
if(!GLAD_GL_EXT_framebuffer_blit) return;
glad_glBlitFramebufferEXT = (PFNGLBLITFRAMEBUFFEREXTPROC) load(userptr, "glBlitFramebufferEXT");
}
static void glad_gl_load_GL_EXT_framebuffer_multisample( GLADuserptrloadfunc load, void* userptr) {
if(!GLAD_GL_EXT_framebuffer_multisample) return;
glad_glRenderbufferStorageMultisampleEXT = (PFNGLRENDERBUFFERSTORAGEMULTISAMPLEEXTPROC) load(userptr, "glRenderbufferStorageMultisampleEXT");
}
static void glad_gl_load_GL_EXT_framebuffer_object( GLADuserptrloadfunc load, void* userptr) {
if(!GLAD_GL_EXT_framebuffer_object) return;
glad_glBindFramebufferEXT = (PFNGLBINDFRAMEBUFFEREXTPROC) load(userptr, "glBindFramebufferEXT");
@@ -2366,6 +2378,8 @@ static int glad_gl_find_extensions_gl(void) {
GLAD_GL_ARB_compatibility = glad_gl_has_extension(exts, exts_i, "GL_ARB_compatibility");
GLAD_GL_ARB_framebuffer_object = glad_gl_has_extension(exts, exts_i, "GL_ARB_framebuffer_object");
GLAD_GL_EXT_framebuffer_blit = glad_gl_has_extension(exts, exts_i, "GL_EXT_framebuffer_blit");
GLAD_GL_EXT_framebuffer_multisample = glad_gl_has_extension(exts, exts_i, "GL_EXT_framebuffer_multisample");
GLAD_GL_EXT_framebuffer_object = glad_gl_has_extension(exts, exts_i, "GL_EXT_framebuffer_object");
GLAD_GL_EXT_texture_compression_s3tc = glad_gl_has_extension(exts, exts_i, "GL_EXT_texture_compression_s3tc");
GLAD_GL_EXT_texture_filter_anisotropic = glad_gl_has_extension(exts, exts_i, "GL_EXT_texture_filter_anisotropic");
@@ -2451,6 +2465,8 @@ int gladLoadGLUserPtr( GLADuserptrloadfunc load, void *userptr) {
if (!glad_gl_find_extensions_gl()) return 0;
glad_gl_load_GL_ARB_framebuffer_object(load, userptr);
glad_gl_load_GL_EXT_framebuffer_blit(load, userptr);
glad_gl_load_GL_EXT_framebuffer_multisample(load, userptr);
glad_gl_load_GL_EXT_framebuffer_object(load, userptr);
@@ -2574,7 +2590,9 @@ static void* glad_gl_dlopen_handle(void) {
"libGL-1.so",
#endif
"libGL.so.1",
"libGL.so"
"libGL.so",
"libEGL.so.1",
"libEGL.so"
};
#endif
@@ -2597,6 +2615,9 @@ static struct _glad_gl_userptr glad_gl_build_userptr(void *handle) {
#else
userptr.gl_get_proc_address_ptr =
(GLADglprocaddrfunc) glad_dlsym_handle(handle, "glXGetProcAddressARB");
if (!userptr.gl_get_proc_address_ptr)
userptr.gl_get_proc_address_ptr =
(GLADglprocaddrfunc) glad_dlsym_handle(handle, "eglGetProcAddress");
#endif
return userptr;
+7 -8
View File
@@ -54,12 +54,12 @@ public:
int & i,
Eigen::Matrix<double, 1, 3> &closest)
{
size_t idx_unsigned = 0;
Vec3d closest_vec3d(closest);
double dist =
size_t idx_unsigned { 0 };
Vec3d closest_vec3d { Vec3d::Zero() };
const double dist {
AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
its.vertices, its.indices, m_tree, point, idx_unsigned,
closest_vec3d);
closest_vec3d) };
i = int(idx_unsigned);
closest = closest_vec3d;
return dist;
@@ -311,10 +311,9 @@ AABBMesh::hit_result IndexedMesh::filter_hits(
double AABBMesh::squared_distance(const Vec3d &p, int& i, Vec3d& c) const {
double sqdst = 0;
Eigen::Matrix<double, 1, 3> pp = p;
Eigen::Matrix<double, 1, 3> cc;
sqdst = m_aabb->squared_distance(*m_tm, pp, i, cc);
const Eigen::Matrix<double, 1, 3> pp { p };
Eigen::Matrix<double, 1, 3> cc { Vec3d::Zero() };
const double sqdst { m_aabb->squared_distance(*m_tm, pp, i, cc) };
c = cc;
return sqdst;
}
+2 -1
View File
@@ -31,8 +31,9 @@ namespace AABBTreeLines {
inline VectorType closest_point_to_origin(size_t primitive_index, ScalarType& squared_distance) const
{
Vec<LineType::Dim, typename LineType::Scalar> nearest_point;
Vec<LineType::Dim, typename LineType::Scalar> cast_origin = origin.template cast<typename LineType::Scalar>();
const LineType& line = lines[primitive_index];
squared_distance = line_alg::distance_to_squared(line, origin.template cast<typename LineType::Scalar>(), &nearest_point);
squared_distance = line_alg::distance_to_squared(line, cast_origin, &nearest_point);
return nearest_point.template cast<ScalarType>();
}
};
+36 -1
View File
@@ -202,6 +202,25 @@ void AppConfig::set_defaults()
if (get("seq_top_layer_only").empty())
set("seq_top_layer_only", "1");
// ORCA: darken the layers the preview layer slider is not scrubbed to
if (get("preview_dim_previous_layers").empty())
set_bool("preview_dim_previous_layers", false);
// ORCA: brightness of those dimmed layers, in percent. 0 = black, capped at 99 because
// 100 would render them unchanged, which is what disabling the option already does
if (get("preview_dim_previous_layers_brightness").empty())
set("preview_dim_previous_layers_brightness", "40");
else {
int brightness = 40;
try {
brightness = std::stoi(get("preview_dim_previous_layers_brightness"));
}
catch (...) {
brightness = 40;
}
set("preview_dim_previous_layers_brightness", std::to_string(std::max(0, std::min(brightness, 99))));
}
if (get("filaments_area_preferred_count").empty())
set("filaments_area_preferred_count", "10");
@@ -304,6 +323,10 @@ void AppConfig::set_defaults()
if (get("show_3d_navigator").empty())
set_bool("show_3d_navigator", true);
// Show the one-time "Filament Track Switch is ready" tip until it has been seen once.
if (get("show_fila_switch_tips").empty())
set_bool("show_fila_switch_tips", true);
if (get("show_plate_gridlines").empty())
set_bool("show_plate_gridlines", true);
@@ -603,6 +626,12 @@ void AppConfig::set_defaults()
set_bool("window_buttons_on_left", false);
#endif
if (get("use_printer_agents").empty())
{
// false = legacy behavior using print hosts
set_bool("use_printer_agents", false);
}
// Remove legacy window positions/sizes
erase("app", "main_frame_maximized");
erase("app", "main_frame_pos");
@@ -800,6 +829,10 @@ std::string AppConfig::load()
preset_info.nozzle_volume_type = NozzleVolumeType(cali_it.value()["nozzle_volume_type"].get<int>());
if (cali_it.value().contains("bed_type"))
preset_info.bed_type = BedType(cali_it.value()["bed_type"].get<int>());
if (cali_it.value().contains("nozzle_pos_id"))
preset_info.nozzle_pos_id = cali_it.value()["nozzle_pos_id"].get<int>();
if (cali_it.value().contains("nozzle_sn"))
preset_info.nozzle_sn = cali_it.value()["nozzle_sn"].get<std::string>();
cali_info.selected_presets.push_back(preset_info);
}
}
@@ -850,7 +883,7 @@ std::string AppConfig::load()
}
}
}
} catch(std::exception err) {
} catch(const std::exception &err) {
BOOST_LOG_TRIVIAL(info) << format("parse app config \"%1%\", error: %2%", AppConfig::loading_path(), err.what());
return err.what();
@@ -957,6 +990,8 @@ void AppConfig::save()
preset_json["extruder_id"] = filament_preset.extruder_id;
preset_json["nozzle_volume_type"] = int(filament_preset.nozzle_volume_type);
preset_json["bed_type"] = int(filament_preset.bed_type);
preset_json["nozzle_pos_id"] = filament_preset.nozzle_pos_id;
preset_json["nozzle_sn"] = filament_preset.nozzle_sn;
preset_json["nozzle_diameter"] = filament_preset.nozzle_diameter;
preset_json["filament_id"] = filament_preset.filament_id;
preset_json["setting_id"] = filament_preset.setting_id;
@@ -1660,7 +1660,7 @@ void SkeletalTrapezoidation::propagateBeadingsDownward(edge_t* edge_to_peak, ptr
}
SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right, coord_t switching_radius) const
SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right, coord_t switching_radius)
{
assert(ratio_left_to_whole >= 0.0 && ratio_left_to_whole <= 1.0);
Beading ret = interpolate(left, ratio_left_to_whole, right);
@@ -1684,6 +1684,12 @@ SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beadin
{ // We cant adjust to fit the next edge because there is no previous one?!
return ret;
}
// ret follows the thicker of left/right, which can hold fewer insets than left when bead
// count and thickness disagree; skip the adjustment rather than index ret past its end.
if (next_inset_idx >= coord_t(ret.toolpath_locations.size()))
{
return ret;
}
assert(next_inset_idx < coord_t(left.toolpath_locations.size()));
assert(left.toolpath_locations[next_inset_idx] <= switching_radius);
assert(left.toolpath_locations[next_inset_idx + 1] >= switching_radius);
@@ -1703,7 +1709,7 @@ SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beadin
}
SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right) const
SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right)
{
assert(ratio_left_to_whole >= 0.0 && ratio_left_to_whole <= 1.0);
float ratio_right_to_whole = 1.0 - ratio_left_to_whole;
@@ -488,7 +488,7 @@ protected:
* beads.
* \return The beading at the interpolated location.
*/
Beading interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right, coord_t switching_radius) const;
static Beading interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right, coord_t switching_radius);
/*!
* Subroutine of \ref interpolate(const Beading&, Ratio, const Beading&, coord_t)
@@ -501,7 +501,7 @@ protected:
* \param right One of the beadings to interpolate between.
* \return The beading at the interpolated location.
*/
Beading interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right) const;
static Beading interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right);
/*!
* Get the beading at a certain node of the skeletal graph, or create one if
+8 -8
View File
@@ -23,14 +23,14 @@ inline coord_t meshfix_maximum_extrusion_area_deviation() { return scaled<coo
class WallToolPathsParams
{
public:
float min_bead_width;
float min_feature_size;
float min_length_factor;
float wall_transition_length;
float wall_transition_angle;
float wall_transition_filter_deviation;
int wall_distribution_count;
bool is_top_or_bottom_layer;
float min_bead_width = 0.f;
float min_feature_size = 0.f;
float min_length_factor = 0.5f;
float wall_transition_length = 0.f;
float wall_transition_angle = 10.f;
float wall_transition_filter_deviation = 0.f;
int wall_distribution_count = 1;
bool is_top_or_bottom_layer = false;
coord_t wall_maximum_resolution = meshfix_maximum_resolution();
coord_t wall_maximum_deviation = meshfix_maximum_deviation();
+2 -2
View File
@@ -25,7 +25,7 @@ public:
min(p1), max(p1), defined(false) { merge(p2); merge(p3); }
template<class It, class = IteratorOnly<It>>
BoundingBoxBase(It from, It to)
BoundingBoxBase(It from, It to) : BoundingBoxBase()
{ construct(*this, from, to); }
BoundingBoxBase(const PointsType &points)
@@ -125,7 +125,7 @@ public:
template<class It, class = IteratorOnly<It> > BoundingBox3Base(It from, It to)
{
if (from == to)
throw Slic3r::InvalidArgument("Empty point set supplied to BoundingBox3Base constructor");
throw Slic3r::InvalidArgument("Empty point set supplied to BoundingBox3Base constructor.");
auto it = from;
this->min = it->template cast<typename PointType::Scalar>();
+80 -163
View File
@@ -32,16 +32,14 @@ static void append_and_translate(ExPolygons &dst, const ExPolygons &src, const P
for (; dst_idx < dst.size(); ++dst_idx)
dst[dst_idx].translate(instance_shift);
}
// BBS: generate brim area by objs
static void append_and_translate(ExPolygons& dst, const ExPolygons& src,
const PrintInstance& instance, const Print& print, std::map<ObjectID, ExPolygons>& brimAreaMap) {
// Orca: Translate the brim area into print coordinates and store it per instance.
static void append_and_translate(const ExPolygons& src, const PrintInstance& instance,
size_t instance_idx, std::map<ObjectInstanceID, ExPolygons>& brimAreaMap) {
ExPolygons srcShifted = src;
Point instance_shift = instance.shift_without_plate_offset();
for (size_t src_idx = 0; src_idx < srcShifted.size(); ++src_idx)
srcShifted[src_idx].translate(instance_shift);
srcShifted = diff_ex(srcShifted, dst);
//expolygons_append(dst, temp2);
expolygons_append(brimAreaMap[instance.print_object->id()], std::move(srcShifted));
for (ExPolygon& expoly : srcShifted)
expoly.translate(instance_shift);
expolygons_append(brimAreaMap[{ instance.print_object->id(), instance_idx }], std::move(srcShifted));
}
static void append_and_translate(Polygons &dst, const Polygons &src, const PrintInstance &instance) {
@@ -61,7 +59,7 @@ static bool use_brim_efc_outline(const PrintObject &object)
&& object.config().raft_layers.value == 0;
}
//ORCA: Helper for snapping painted ears to the EFC outline.
//ORCA: Helper for projecting painted ears to the EFC outline.
static bool closest_point_on_expolygons(const ExPolygons &polygons, const Point &from, Point &closest_out)
{
double min_dist2 = std::numeric_limits<double>::max();
@@ -69,23 +67,22 @@ static bool closest_point_on_expolygons(const ExPolygons &polygons, const Point
for (const ExPolygon &poly : polygons) {
for (int i = 0; i < poly.num_contours(); ++i) {
const Point *candidate = poly.contour_or_hole(i).closest_point(from);
if (candidate == nullptr)
continue;
const int64_t dx = int64_t(candidate->x()) - int64_t(from.x());
const int64_t dy = int64_t(candidate->y()) - int64_t(from.y());
const double dist2 = double(dx * dx + dy * dy);
if (dist2 < min_dist2) {
min_dist2 = dist2;
closest_out = *candidate;
found = true;
const Lines lines = poly.contour_or_hole(i).lines();
for (const Line &line : lines) {
Point candidate;
const double dist2 = line.distance_to_squared(from, &candidate);
if (dist2 < min_dist2) {
min_dist2 = dist2;
closest_out = candidate;
found = true;
}
}
}
}
return found;
}
//ORCA: Helper for matching painted ears to their original island before EFC snapping.
//ORCA: Helper for matching painted ears to their original island before EFC projection.
static int find_containing_expolygon_index(const ExPolygons &polygons, const Point &from)
{
for (size_t idx = 0; idx < polygons.size(); ++idx) {
@@ -95,7 +92,7 @@ static int find_containing_expolygon_index(const ExPolygons &polygons, const Poi
return -1;
}
//ORCA: Keep painted ear snapping on the matching island when using EFC outline.
//ORCA: Keep painted ear projection on the matching island when using EFC outline.
static bool closest_point_on_matching_island(const ExPolygons &raw_outline, const ExPolygons &efc_outline, const Point &from, Point &closest_out)
{
const int island_idx = find_containing_expolygon_index(raw_outline, from);
@@ -106,6 +103,7 @@ static bool closest_point_on_matching_island(const ExPolygons &raw_outline, cons
}
return closest_point_on_expolygons(efc_outline, from, closest_out);
}
//ORCA: Use post-processed first-layer slices (including EFC) for brim outline.
// Returns ExPolygons of the bottom layer after all first-layer modifiers
// (including elephant foot compensation, if enabled) have been applied.
@@ -351,18 +349,19 @@ static ExPolygons make_brim_ears_auto(const ExPolygons& obj_expoly, coord_t size
return mouse_ears_ex;
}
static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWidth, float brim_offset, Flow &flow, bool is_outer_brim)
static ExPolygons make_brim_ears(const PrintObject* object)
{
ExPolygons mouse_ears_ex;
BrimPoints brim_ear_points = object->model_object()->brim_points;
if (brim_ear_points.size() <= 0) {
return mouse_ears_ex;
}
//ORCA: Painted ears can snap to the EFC-adjusted outline when enabled.
//ORCA: Painted ears follow the EFC-adjusted outline when enabled, while
// preserving their position along the selected outline segment.
const bool use_efc_outline = use_brim_efc_outline(*object);
const ExPolygons &raw_outline = object->layers().front()->lslices;
//ORCA: Lazily computed EFC-adjusted bottom outline.
//Stored separately so we can avoid recomputation unless EFC snapping is used.
//Stored separately so we can avoid recomputation unless EFC projection is used.
ExPolygons efc_outline_storage;
const ExPolygons* efc_outline = nullptr;
@@ -374,12 +373,7 @@ static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWi
Vec3f world_pos = pt.transform(trsf.get_matrix());
if ( world_pos.z() > 0) continue;
Polygon point_round;
float brim_width = floor(scale_(pt.head_front_radius) / flowWidth / 2) * flowWidth * 2;
if (is_outer_brim) {
double flowWidthScale = flowWidth / SCALING_FACTOR;
brim_width = floor(brim_width / flowWidthScale / 2) * flowWidthScale * 2;
}
coord_t size_ear = (brim_width - brim_offset - flow.scaled_spacing());
const coord_t size_ear = scale_(pt.head_front_radius);
for (size_t i = 0; i < POLY_SIDE_COUNT; i++) {
double angle = (2.0 * PI * i) / POLY_SIDE_COUNT;
point_round.points.emplace_back(size_ear * cos(angle), size_ear * sin(angle));
@@ -390,17 +384,17 @@ static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWi
int32_t pt_x = scale_(pos.x());
int32_t pt_y = scale_(pos.y());
//ORCA: Snap painted ears to the EFC-adjusted outline when enabled.
//ORCA: Project painted ears to the EFC-adjusted outline when enabled.
if (use_efc_outline) {
if (efc_outline == nullptr) {
//ORCA: Compute EFC-adjusted outline lazily for painted ear snapping.
//ORCA: Compute the EFC-adjusted outline lazily for painted ear projection.
efc_outline_storage = get_print_object_bottom_layer_expolygons(*object);
efc_outline = &efc_outline_storage;
}
if (!efc_outline->empty()) {
Point closest_point;
//ORCA: Snap within the matching island to avoid drifting to another island.
//ORCA: Project within the matching island to avoid drifting to another island.
if (closest_point_on_matching_island(
raw_outline,
*efc_outline,
@@ -419,8 +413,7 @@ static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWi
//BBS: create all brims
static ExPolygons outer_inner_brim_area(const Print& print,
const float no_brim_offset, std::map<ObjectID, ExPolygons>& brimAreaMap,
std::map<ObjectID, ExPolygons>& supportBrimAreaMap,
const float no_brim_offset, std::map<ObjectInstanceID, ExPolygons>& brimAreaMap,
std::vector<std::pair<ObjectID, unsigned int>>& objPrintVec,
std::vector<unsigned int>& printExtruders)
{
@@ -454,7 +447,8 @@ static ExPolygons outer_inner_brim_area(const Print& print,
bool has_brim_auto = object->config().brim_type == btAutoBrim;
const bool use_auto_brim_ears = object->config().brim_type == btEar;
const bool use_brim_ears = object->config().brim_type == btPainted;
const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_auto_brim_ears || use_brim_ears;
const bool use_inner_brim_ears = (use_auto_brim_ears || use_brim_ears) && !object->config().brim_ears_outer_only.value;
const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_inner_brim_ears;
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || use_auto_brim_ears || use_brim_ears;
coord_t ear_detection_length = scale_(object->config().brim_ears_detection_length.value);
coordf_t brim_ears_max_angle = object->config().brim_ears_max_angle.value;
@@ -469,7 +463,6 @@ static ExPolygons outer_inner_brim_area(const Print& print,
ExPolygons brim_area_object;
ExPolygons no_brim_area_object;
ExPolygons brim_area_support;
ExPolygons no_brim_area_support;
Polygons holes_object;
Polygons holes_support;
@@ -534,7 +527,7 @@ static ExPolygons outer_inner_brim_area(const Print& print,
auto innerExpoly = offset_ex(ex_poly.contour, brim_offset, jtRound, SCALED_RESOLUTION);
ExPolygons outerExpoly;
if (use_brim_ears) {
outerExpoly = make_brim_ears(object, flowWidth, brim_offset, flow, true);
outerExpoly = make_brim_ears(object);
//outerExpoly = offset_ex(outerExpoly, brim_width_mod, jtRound, SCALED_RESOLUTION);
} else if (use_auto_brim_ears) {
coord_t size_ear = (brim_width_mod - brim_offset - flow.scaled_spacing());
@@ -548,7 +541,7 @@ static ExPolygons outer_inner_brim_area(const Print& print,
ExPolygons outerExpoly;
auto innerExpoly = offset_ex(ex_poly_holes_reversed, -brim_width - brim_offset);
if (use_brim_ears) {
outerExpoly = make_brim_ears(object, flowWidth, brim_offset, flow, false);
outerExpoly = make_brim_ears(object);
} else if (use_auto_brim_ears) {
coord_t size_ear = (brim_width - brim_offset - flow.scaled_spacing());
outerExpoly = make_brim_ears_auto(offset_ex(ex_poly_holes_reversed, -brim_offset), size_ear, ear_detection_length, brim_ears_max_angle, false);
@@ -570,16 +563,18 @@ static ExPolygons outer_inner_brim_area(const Print& print,
append(no_brim_area_object, objectIsland);
brimToWrite.at(object->id()).obj = false;
for (const PrintInstance& instance : object->instances()) {
for (size_t instance_idx = 0; instance_idx < object->instances().size(); ++instance_idx) {
const PrintInstance& instance = object->instances()[instance_idx];
if (!brim_area_object.empty())
append_and_translate(brim_area, brim_area_object, instance, print, brimAreaMap);
append_and_translate(brim_area_object, instance, instance_idx, brimAreaMap);
append_and_translate(no_brim_area, no_brim_area_object, instance);
append_and_translate(holes, holes_object, instance);
append_and_translate(objectIslands, objectIsland, instance);
}
if (brimAreaMap.find(object->id()) != brimAreaMap.end())
expolygons_append(brim_area, brimAreaMap[object->id()]);
for (const auto& [key, areas] : brimAreaMap)
if (key.object_id == object->id())
expolygons_append(brim_area, areas);
}
support_material_extruder = object->config().support_filament;
if (support_material_extruder == 0 && object->has_support_material()) {
@@ -591,32 +586,12 @@ static ExPolygons outer_inner_brim_area(const Print& print,
if (support_material_extruder == extruderNo && brimToWrite.at(object->id()).sup) {
if (!object->support_layers().empty() && object->support_layers().front()->support_type==stInnerNormal) {
for (const Polygon& support_contour : object->support_layers().front()->support_fills.polygons_covered_by_spacing()) {
// Brim will not be generated for supports
/*
if (has_outer_brim) {
append(brim_area_support, diff_ex(offset_ex(support_contour, brim_width + brim_offset, jtRound, SCALED_RESOLUTION), offset_ex(support_contour, brim_offset)));
}
if (has_inner_brim || has_outer_brim)
append(no_brim_area_support, offset_ex(support_contour, 0));
*/
no_brim_area_support.emplace_back(support_contour);
}
}
// BBS
if (!object->support_layers().empty() && object->support_layers().front()->support_type == stInnerTree) {
for (const ExPolygon &ex_poly : object->support_layers().front()->lslices) {
// BBS: additional brim width will be added if adhesion area is too small without brim
float brim_width_mod = ex_poly.area() / ex_poly.contour.length() < scaled_half_min_adh_length
&& brim_width < scaled_flow_width ? brim_width + scaled_additional_brim_width : brim_width;
brim_width_mod = floor(brim_width_mod / scaled_flow_width / 2) * scaled_flow_width * 2;
// Brim will not be generated for supports
/*
if (has_outer_brim) {
append(brim_area_support, diff_ex(offset_ex(ex_poly.contour, brim_width_mod + brim_offset, jtRound, SCALED_RESOLUTION), offset_ex(ex_poly.contour, brim_offset)));
}
if (has_inner_brim)
append(brim_area_support, diff_ex(offset_ex(ex_poly.holes, -brim_offset), offset_ex(ex_poly.holes, -brim_width - brim_offset)));
*/
if (!has_outer_brim)
append(no_brim_area_support, diff_ex(offset(ex_poly.contour, no_brim_offset), ex_poly.holes));
if (!has_inner_brim && !has_outer_brim)
@@ -629,13 +604,9 @@ static ExPolygons outer_inner_brim_area(const Print& print,
}
brimToWrite.at(object->id()).sup = false;
for (const PrintInstance& instance : object->instances()) {
if (!brim_area_support.empty())
append_and_translate(brim_area, brim_area_support, instance, print, supportBrimAreaMap);
append_and_translate(no_brim_area, no_brim_area_support, instance);
append_and_translate(holes, holes_support, instance);
}
if (supportBrimAreaMap.find(object->id()) != supportBrimAreaMap.end())
expolygons_append(brim_area, supportBrimAreaMap[object->id()]);
}
}
}
@@ -675,28 +646,27 @@ static ExPolygons outer_inner_brim_area(const Print& print,
}
if (brimAreaMap.find(object->id()) != brimAreaMap.end()) {
brimAreaMap[object->id()] = diff_ex(brimAreaMap[object->id()], extruder_no_brim_area);
}
for (auto& [key, areas] : brimAreaMap)
if (key.object_id == object->id())
areas = diff_ex(areas, extruder_no_brim_area);
if (supportBrimAreaMap.find(object->id()) != supportBrimAreaMap.end())
supportBrimAreaMap[object->id()] = diff_ex(supportBrimAreaMap[object->id()], extruder_no_brim_area);
}
brim_area.clear();
for (const PrintObject* object : print.objects()) {
// BBS: brim should be contacted to at least one object's island or brim area
if (brimAreaMap.find(object->id()) != brimAreaMap.end()) {
for (auto map_it = brimAreaMap.begin(); map_it != brimAreaMap.end(); ++map_it) {
if (map_it->first.object_id != object->id())
continue;
// find other objects' brim area
ExPolygons otherExPolys;
for (const PrintObject* otherObject : print.objects()) {
if ((otherObject->id() != object->id()) && (brimAreaMap.find(otherObject->id()) != brimAreaMap.end())) {
expolygons_append(otherExPolys, brimAreaMap[otherObject->id()]);
}
}
for (const auto& [other_key, other_areas] : brimAreaMap)
if (other_key != map_it->first)
expolygons_append(otherExPolys, other_areas);
auto tempArea = brimAreaMap[object->id()];
brimAreaMap[object->id()].clear();
auto tempArea = map_it->second;
map_it->second.clear();
for (int ia = 0; ia != tempArea.size(); ++ia) {
// find this object's other brim area
@@ -708,9 +678,9 @@ static ExPolygons outer_inner_brim_area(const Print& print,
if (!intersection_ex(offsetedTa, objectIslands).empty() ||
!intersection_ex(offsetedTa, otherExPoly).empty() ||
!intersection_ex(offsetedTa, otherExPolys).empty())
brimAreaMap[object->id()].push_back(tempArea[ia]);
map_it->second.push_back(tempArea[ia]);
}
expolygons_append(brim_area, brimAreaMap[object->id()]);
expolygons_append(brim_area, map_it->second);
}
}
return brim_area;
@@ -889,19 +859,23 @@ ExtrusionEntityCollection makeBrimInfillFromPlateCoordinates(const ExPolygons& s
//BBS: an overload of the orignal brim generator that generates the brim by obj and by extruders
void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_area,
std::map<ObjectID, ExtrusionEntityCollection>& brimMap,
std::map<ObjectID, ExtrusionEntityCollection>& supportBrimMap,
std::map<ObjectInstanceID, ExtrusionEntityCollection>& brimMapByInstance,
std::vector<std::pair<ObjectID, unsigned int>> &objPrintVec,
std::vector<unsigned int>& printExtruders,
std::map<ObjectID, ExPolygons>* objectBrimAreasOut,
std::map<ObjectID, ExPolygons>* supportBrimAreasOut)
std::map<ObjectInstanceID, ExPolygons>* objectBrimAreasByInstanceOut)
{
std::map<ObjectID, double> brim_width_map;
std::map<ObjectID, ExPolygons> brimAreaMap;
std::map<ObjectID, ExPolygons> supportBrimAreaMap;
std::map<ObjectInstanceID, ExPolygons> brimAreaMap;
Flow flow = print.brim_flow();
const auto scaled_resolution = scaled<double>(print.config().resolution.value);
ExPolygons islands_area_ex = outer_inner_brim_area(print,
float(flow.scaled_spacing()), brimAreaMap, supportBrimAreaMap, objPrintVec, printExtruders);
float(flow.scaled_spacing()), brimAreaMap, objPrintVec, printExtruders);
if (!print.config().combine_brims) {
ExPolygons claimed_area;
for (auto& [_, areas] : brimAreaMap) {
areas = diff_ex(areas, claimed_area);
expolygons_append(claimed_area, areas);
}
}
// BBS: Find boundingbox of the first layer
for (const ObjectID printObjID : print.print_object_ids()) {
@@ -923,14 +897,10 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
ex_poly_translated.translate(instance.shift_without_plate_offset());
bbx.merge(get_extents(ex_poly_translated));
}
if (supportBrimAreaMap.find(printObjID) != supportBrimAreaMap.end()) {
for (const ExPolygon& ex_poly : supportBrimAreaMap.at(printObjID))
bbx.merge(get_extents(ex_poly.contour));
}
if (brimAreaMap.find(printObjID) != brimAreaMap.end()) {
for (const ExPolygon& ex_poly : brimAreaMap.at(printObjID))
bbx.merge(get_extents(ex_poly.contour));
}
for (const auto& [key, areas] : brimAreaMap)
if (key.object_id == printObjID)
for (const ExPolygon& ex_poly : areas)
bbx.merge(get_extents(ex_poly.contour));
object->firstLayerObjectBrimBoundingBox = bbx;
}
@@ -943,77 +913,24 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
islands_area[iia].translate(plate_shift);
// Orca: keep translated brim footprints for skirt grouping.
auto translate_area_map = [plate_shift](const std::map<ObjectID, ExPolygons>& src) {
std::map<ObjectID, ExPolygons> dst = src;
auto translate_area_map = [plate_shift](const auto& src) {
auto dst = src;
for (auto& [_, areas] : dst)
for (ExPolygon& area : areas)
area.translate(plate_shift);
return dst;
};
if (objectBrimAreasOut != nullptr)
*objectBrimAreasOut = translate_area_map(brimAreaMap);
if (supportBrimAreasOut != nullptr)
*supportBrimAreasOut = translate_area_map(supportBrimAreaMap);
if (objectBrimAreasByInstanceOut != nullptr)
*objectBrimAreasByInstanceOut = translate_area_map(brimAreaMap);
const bool has_per_object_skirt_or_shield = print.config().skirt_type == stPerObject &&
(print.has_skirt() || print.has_infinite_skirt());
const bool combine_brims = print.config().combine_brims.value &&
!has_per_object_skirt_or_shield &&
print.config().print_sequence != PrintSequence::ByObject;
if (!combine_brims) {
// Orca: Generate brims separately when brims cannot be combined.
for (auto iter = brimAreaMap.begin(); iter != brimAreaMap.end(); ++iter) {
if (!iter->second.empty()) {
brimMap.insert(std::make_pair(iter->first, makeBrimInfill(iter->second, print, islands_area)));
};
}
for (auto iter = supportBrimAreaMap.begin(); iter != supportBrimAreaMap.end(); ++iter) {
if (!iter->second.empty()) {
supportBrimMap.insert(std::make_pair(iter->first, makeBrimInfill(iter->second, print, islands_area)));
};
}
} else {
// Orca: Unified brim mode.
ExPolygons all_brims_merged;
std::vector<ObjectID> brim_object_ids;
// Add all object brims
for (auto& [obj_id, brims] : brimAreaMap) {
if (!brims.empty()) {
expolygons_append(all_brims_merged, brims);
brim_object_ids.push_back(obj_id);
}
}
if (!all_brims_merged.empty()) {
// Merge all brims into a single continuous area
all_brims_merged = union_ex(all_brims_merged);
// Apply a tiny morphological cleanup to reduce boolean-union micro-artifacts.
const float brim_cleanup_delta = std::max(float(scaled_resolution), float(SCALED_EPSILON));
all_brims_merged = offset2_ex(all_brims_merged, brim_cleanup_delta, -brim_cleanup_delta, jtRound, scaled_resolution);
// Generate infill once for the merged brim area.
ExtrusionEntityCollection merged_brim = makeBrimInfill(all_brims_merged, print, islands_area);
// In unified mode, assign the merged brim to a deterministic carrier object.
// Pick the first object in print order that actually contributed brim area.
ObjectID carrier_id;
bool carrier_found = false;
for (const auto& [obj_id, _extruder] : objPrintVec) {
if (std::find(brim_object_ids.begin(), brim_object_ids.end(), obj_id) != brim_object_ids.end()) {
carrier_id = obj_id;
carrier_found = true;
break;
}
}
if (!carrier_found)
carrier_id = brim_object_ids.front();
brimMap[carrier_id] = std::move(merged_brim);
}
// Orca: Generate brims per object instance. If Combine brims is enabled,
// Print::_make_skirt() will join the touching ones.
for (auto iter = brimAreaMap.begin(); iter != brimAreaMap.end(); ++iter) {
if (!iter->second.empty()) {
ExtrusionEntityCollection brim = makeBrimInfill(iter->second, print, islands_area);
brimMap[iter->first.object_id].append(brim.entities);
brimMapByInstance.emplace(iter->first, std::move(brim));
};
}
}
} // namespace Slic3r
+3 -4
View File
@@ -2,6 +2,7 @@
#define slic3r_Brim_hpp_
#include "ExPolygon.hpp"
#include "ObjectID.hpp"
#include "Point.hpp"
#include<map>
@@ -12,17 +13,15 @@ namespace Slic3r {
class Print;
class ExtrusionEntityCollection;
class PrintTryCancel;
class ObjectID;
// Produce brim lines around those objects, that have the brim enabled.
// Collect islands_area to be merged into the final 1st layer convex hull.
void make_brim(const Print& print, PrintTryCancel try_cancel,
Polygons& islands_area, std::map<ObjectID, ExtrusionEntityCollection>& brimMap,
std::map<ObjectID, ExtrusionEntityCollection>& supportBrimMap,
std::map<ObjectInstanceID, ExtrusionEntityCollection>& brimMapByInstance,
std::vector<std::pair<ObjectID, unsigned int>>& objPrintVec,
std::vector<unsigned int>& printExtruders,
std::map<ObjectID, ExPolygons>* objectBrimAreasOut = nullptr,
std::map<ObjectID, ExPolygons>* supportBrimAreasOut = nullptr);
std::map<ObjectInstanceID, ExPolygons>* objectBrimAreasByInstanceOut = nullptr);
ExtrusionEntityCollection makeBrimInfill(const ExPolygons& singleBrimArea, const Print& print, const Polygons& islands_area);
ExtrusionEntityCollection makeBrimInfillFromPlateCoordinates(const ExPolygons& singleBrimArea, const Print& print, const Polygons& islands_area);
+14 -4
View File
@@ -19,6 +19,7 @@ if (TARGET OpenVDB::openvdb)
endif()
option(BUILD_SHARED_LIBS "Build shared libs" OFF)
option(USE_SLIC3R_CONSOLE_LOG "Enable console logging in RelWithDebInfo builds" OFF)
set(lisbslic3r_sources
AABBMesh.cpp
@@ -221,11 +222,10 @@ set(lisbslic3r_sources
GCode/FanMover.hpp
GCode/GCodeProcessor.cpp
GCode/GCodeProcessor.hpp
GCode/ElegooGCodeProcessorHelper.cpp
GCode.hpp
GCode/PchipInterpolatorHelper.cpp
GCode/PchipInterpolatorHelper.hpp
GCode/PostProcessor.cpp
GCode/PostProcessor.hpp
GCode/PressureEqualizer.cpp
GCode/PressureEqualizer.hpp
GCode/PrintExtents.cpp
@@ -248,6 +248,8 @@ set(lisbslic3r_sources
GCode/Thumbnails.hpp
GCode/ToolOrdering.cpp
GCode/ToolOrdering.hpp
GCode/OrderingStrategies.cpp
GCode/OrderingStrategies.hpp
GCode/WipeTower2.cpp
GCode/WipeTower2.hpp
GCode/WipeTower.cpp
@@ -470,6 +472,8 @@ set(lisbslic3r_sources
FilamentGroup.cpp
FilamentGroupUtils.hpp
FilamentGroupUtils.cpp
MultiNozzleUtils.hpp
MultiNozzleUtils.cpp
GCode/ToolOrderUtils.hpp
GCode/ToolOrderUtils.cpp
FlushVolPredictor.hpp
@@ -496,8 +500,12 @@ set(CGAL_DO_NOT_WARN_ABOUT_CMAKE_BUILD_TYPE ON CACHE BOOL "" FORCE)
cmake_policy(PUSH)
cmake_policy(SET CMP0011 NEW)
# CGAL's config resets policies (cmake_minimum_required ...3.23), so a plain SET
# can't reach it; the default opts its Boost lookup into BoostConfig (CMP0167).
set(CMAKE_POLICY_DEFAULT_CMP0167 NEW)
find_package(CGAL REQUIRED)
find_package(OpenCV REQUIRED core)
unset(CMAKE_POLICY_DEFAULT_CMP0167)
cmake_policy(POP)
add_library(libslic3r_cgal STATIC
@@ -534,7 +542,9 @@ endif ()
encoding_check(libslic3r)
target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTION=0)
target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>)
if (USE_SLIC3R_CONSOLE_LOG)
target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>)
endif()
target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(libslic3r SYSTEM PUBLIC ${EXPAT_INCLUDE_DIRS})
@@ -544,7 +554,7 @@ find_package(OpenCASCADE REQUIRED)
target_include_directories(libslic3r SYSTEM PUBLIC ${OpenCASCADE_INCLUDE_DIR})
find_package(JPEG REQUIRED)
find_package(draco REQUIRED)
find_package(Draco REQUIRED)
set(OCCT_LIBS
TKXDESTEP
+12
View File
@@ -188,6 +188,18 @@ ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta)
return results;
}
ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta, Clipper2Lib::JoinType joinType)
{
Clipper2Lib::Paths64 subject = Slic3rExPolygons_to_Paths64(expolygons);
Clipper2Lib::ClipperOffset offsetter;
offsetter.AddPaths(subject, joinType, Clipper2Lib::EndType::Polygon);
Clipper2Lib::PolyPath64 polytree;
offsetter.Execute(delta, polytree);
ExPolygons results = PolyTreeToExPolygons(std::move(polytree));
return results;
}
ExPolygons offset2_ex_2(const ExPolygons& expolygons, double delta1, double delta2)
{
// 1st offset
+1
View File
@@ -15,6 +15,7 @@ Slic3r::Polylines diff_pl_2(const Slic3r::Polylines& subject, const Slic3r::Pol
ExPolygons union_ex_2(const Polygons &expolygons);
ExPolygons union_ex_2(const ExPolygons &expolygons);
ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta);
ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta, Clipper2Lib::JoinType joinType);
ExPolygons offset2_ex_2(const ExPolygons &expolygons, double delta1, double delta2);
}
+75
View File
@@ -1,3 +1,7 @@
#include <limits>
#include <numeric>
#include <unordered_map>
#include "ClipperUtils.hpp"
#include "Geometry.hpp"
#include "ShortestPath.hpp"
@@ -930,6 +934,77 @@ Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r
Slic3r::Polylines intersection_pl(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip)
{ return _clipper_pl_closed(ClipperLib::ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip)); }
// Orca: Sort and orient open polyline fragments produced by clipping `source` with
// intersection_pl(), so that they run in the same order and direction as the source
// polyline. Clipping creates new endpoints at the clip boundary, but it keeps the
// interior source vertices intact, so a fragment's position on the source path is
// recovered exactly by looking its vertices up in the source. Fragments without any
// surviving source vertex lie on a single source segment, found by a nearest-segment
// search.
void restore_source_path_order(const Slic3r::Polyline &source, Slic3r::Polylines &fragments)
{
const Points &src = source.points;
if (src.size() < 2 || fragments.empty())
return;
std::unordered_map<Point, size_t, PointHash> source_index;
source_index.reserve(src.size());
for (size_t i = 0; i < src.size(); ++ i)
source_index.emplace(src[i], i);
// Sort key: index of the source vertex where the fragment starts, then the signed
// offset of the fragment's start from that vertex, to order multiple fragments cut
// from one long source segment.
std::vector<std::pair<size_t, double>> keys(fragments.size());
for (size_t n = 0; n < fragments.size(); ++ n) {
Polyline &pl = fragments[n];
const size_t npos = size_t(-1);
size_t front = npos;
size_t back = npos;
for (const Point &pt : pl.points)
if (auto it = source_index.find(pt); it != source_index.end()) {
front = it->second;
break;
}
for (auto i = pl.points.rbegin(); i != pl.points.rend(); ++ i)
if (auto it = source_index.find(*i); it != source_index.end()) {
back = it->second;
break;
}
Vec2crd source_dir;
if (front == npos) {
// All vertices were created by clipping, thus the whole fragment lies on a
// single source segment. Find that segment.
double best = std::numeric_limits<double>::max();
for (size_t i = 0; i + 1 < src.size(); ++ i)
if (double d = Line::distance_to_squared(pl.first_point(), src[i], src[i + 1]); d < best) {
best = d;
front = i;
}
back = front;
source_dir = src[front + 1] - src[front];
} else
source_dir = src[std::min(back + 1, src.size() - 1)] - src[front > 0 ? front - 1 : 0];
if (front > back) {
pl.reverse();
std::swap(front, back);
} else if (front == back &&
(pl.last_point() - pl.first_point()).cast<double>().dot(source_dir.cast<double>()) < 0.)
pl.reverse();
const Vec2crd seg = src[std::min(front + 1, src.size() - 1)] - src[front];
keys[n] = { front, (pl.first_point() - src[front]).cast<double>().dot(seg.cast<double>()) };
}
std::vector<size_t> order(fragments.size());
std::iota(order.begin(), order.end(), size_t(0));
std::sort(order.begin(), order.end(), [&keys](size_t a, size_t b) { return keys[a] < keys[b]; });
Polylines sorted;
sorted.reserve(fragments.size());
for (size_t n : order)
sorted.emplace_back(std::move(fragments[n]));
fragments = std::move(sorted);
}
Lines _clipper_ln(ClipperLib::ClipType clipType, const Lines &subject, const Polygons &clip)
{
// convert Lines to Polylines
+4
View File
@@ -528,6 +528,10 @@ Slic3r::Polylines intersection_pl(const Slic3r::Polygons &subject, const Slic3r
Slic3r::Polylines3 intersection_pl(const Slic3r::Polylines3 &subject, const Slic3r::Polygon &clip);
Slic3r::Polylines3 intersection_pl(const Slic3r::Polylines3 &subject, const Slic3r::ExPolygon &clip);
// Orca: Sort and orient open polyline fragments produced by clipping `source` with
// intersection_pl(), so that they run in the same order and direction as the source polyline.
void restore_source_path_order(const Slic3r::Polyline &source, Slic3r::Polylines &fragments);
inline Slic3r::Lines intersection_ln(const Slic3r::Lines &subject, const Slic3r::Polygons &clip)
{
return _clipper_ln(ClipperLib::ctIntersection, subject, clip);
+104 -3
View File
@@ -4,6 +4,7 @@
#include "LocalesUtils.hpp"
#include "Preset.hpp"
#include <algorithm>
#include <assert.h>
#include <fstream>
#include <iostream>
@@ -36,6 +37,8 @@ using namespace nlohmann;
namespace Slic3r {
std::function<std::string(std::string, std::string)> ConfigBase::resolve_capability_fn = nullptr;
//BBS: add json support
//static const std::string CONFIG_VERSION_KEY = "version";
//static const std::string CONFIG_NAME_KEY = "name";
@@ -84,12 +87,12 @@ std::string escape_strings_cstyle(const std::vector<std::string> &strs)
// Separate the strings.
(*outptr ++) = ';';
const std::string &str = strs[j];
// Is the string simple or complex? Complex string contains spaces, tabs, new lines and other
// escapable characters. Empty string shall be quoted as well, if it is the only string in strs.
// Is the string simple or complex? Complex string contains spaces, tabs, semicolons, new lines
// and other escapable characters. Empty string shall be quoted as well, if it is the only string in strs.
bool should_quote = strs.size() == 1 && str.empty();
for (size_t i = 0; i < str.size(); ++ i) {
char c = str[i];
if (c == ' ' || c == '\t' || c == '\\' || c == '"' || c == '\r' || c == '\n') {
if (c == ' ' || c == '\t' || c == ';' || c == '\\' || c == '"' || c == '\r' || c == '\n') {
should_quote = true;
break;
}
@@ -1486,6 +1489,29 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
return std::move(substitutions_ctxt.substitutions);
}
std::optional<PluginCapabilityRef> parse_capability_ref(const std::string& value)
{
// Capability references are stored as "<plugin_name>;<cloud_uuid>;<capability_name>".
// The cloud UUID is empty for local plugins (two consecutive semicolons).
if (value.empty())
return std::nullopt;
const size_t first = value.find(';');
if (first == std::string::npos)
return std::nullopt;
const size_t second = value.find(';', first + 1);
if (second == std::string::npos)
return std::nullopt;
std::string name = value.substr(0, first);
std::string uuid = value.substr(first + 1, second - first - 1);
std::string capability_name = value.substr(second + 1);
if (name.empty() || capability_name.empty())
return std::nullopt;
return PluginCapabilityRef{ std::move(name), std::move(capability_name), std::move(uuid) };
}
//BBS: add json support
void ConfigBase::save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const
{
@@ -1522,6 +1548,18 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
}
}
// Serialize the top-level "plugins" manifest: the individual plugin-backed options keep bare
// capability names; the full "name;uuid;capability" references are derived here (same helper as
// update_plugin_manifest). Only with a resolver (GUI); without one (CLI/headless) leave whatever
// the "plugins" option already serialized above, so a round-trip never drops the manifest.
if (resolve_capability_fn) {
std::vector<std::string> unique_refs = this->collect_plugin_manifest();
if (unique_refs.empty())
j.erase("plugins");
else
j["plugins"] = unique_refs;
}
boost::nowide::ofstream c;
c.open(file, std::ios::out | std::ios::trunc);
c << j.dump(1, '\t') << std::endl;
@@ -1553,6 +1591,69 @@ void ConfigBase::null_nullables()
}
}
void ConfigBase::save_plugin_collection(const std::string& opt_key, const ConfigOption* opt, std::vector<std::string>& plugin_refs) const {
// Full plugin capability references ("name;uuid;capability") can only be derived through the
// resolver registered by the GUI once plugins are loaded. In non-GUI/headless contexts (e.g.
// the CLI) it stays null, so skip silently rather than calling an empty std::function.
if (!resolve_capability_fn)
return;
// A plugin-backed option declares its capability type via ConfigOptionDef::plugin_type (the same
// metadata PluginResolver::find_option_for_capability scans). Deriving off the def rather than a
// per-key branch keeps this generic across every plugin-backed option.
const ConfigDef* def = this->def();
const ConfigOptionDef* opt_def = def ? def->get(opt_key) : nullptr;
if (opt_def == nullptr || !opt_def->is_plugin_backed())
return;
const std::string& type = opt_def->plugin_type;
// Resolve a single bare capability value into its full reference and append it, skipping unset
// values, capabilities that could not be resolved (resolver returns ""), and duplicates already
// collected (preserving insertion order).
const auto append_ref = [&plugin_refs, &type](const std::string& capability_value) {
if (capability_value.empty())
return;
std::string ref = resolve_capability_fn(capability_value, type);
if (!ref.empty() && std::find(plugin_refs.begin(), plugin_refs.end(), ref) == plugin_refs.end())
plugin_refs.emplace_back(std::move(ref));
};
// Scalar options carry a single capability name; vector options carry a list. Same scalar/vector
// dispatch as PluginResolver::find_option_for_capability.
if (const auto* string_option = dynamic_cast<const ConfigOptionString*>(opt))
append_ref(string_option->value);
else if (const auto* vector_option = dynamic_cast<const ConfigOptionVectorBase*>(opt))
for (const std::string& val : vector_option->vserialize())
append_ref(val);
}
std::vector<std::string> ConfigBase::collect_plugin_manifest() const
{
std::vector<std::string> refs;
if (!resolve_capability_fn)
return refs;
// Each plugin-backed option (ConfigOptionDef::is_plugin_backed) contributes its resolved
// reference(s) via save_plugin_collection, which appends in order and skips duplicates, so no
// second de-duplication pass is needed here.
for (const std::string& opt_key : this->keys())
if (const ConfigOption* opt = this->option(opt_key))
this->save_plugin_collection(opt_key, opt, refs);
return refs;
}
void ConfigBase::update_plugin_manifest()
{
// Writes the derived manifest back into this config's "plugins" option (save_to_json writes the
// same manifest into a JSON document instead), so an in-memory backend config carries a resolved
// manifest even when the source preset was never serialized (picked-but-unsaved). Without a
// resolver (CLI/headless) leave whatever manifest was loaded from disk untouched.
if (!resolve_capability_fn)
return;
if (auto* manifest = this->option<ConfigOptionStrings>("plugins", true))
manifest->values = this->collect_plugin_manifest();
}
DynamicConfig::DynamicConfig(const ConfigBase& rhs, const t_config_option_keys& keys)
{
for (const t_config_option_key& opt_key : keys)
+85 -5
View File
@@ -2,6 +2,7 @@
#define slic3r_Config_hpp_
#include <assert.h>
#include <algorithm>
#include <map>
#include <climits>
#include <cfloat>
@@ -9,6 +10,7 @@
#include <cstdlib>
#include <functional>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
@@ -363,6 +365,7 @@ public:
virtual void set_with_restore(const ConfigOptionVectorBase* rhs, std::vector<int>& restore_index, int stride) = 0;
virtual void set_with_restore_2(const ConfigOptionVectorBase* rhs, std::vector<int>& restore_index, int start, int len, bool skip_error = false) = 0;
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride) = 0;
virtual void set_to_index(const ConfigOptionVectorBase* rhs, std::vector<int>& dest_index, int stride) = 0;
virtual void set_with_nil(const ConfigOptionVectorBase* rhs, const ConfigOptionVectorBase* inherits, int stride) = 0;
// Resize the vector of values, copy the newly added values from opt_default if provided.
virtual void resize(size_t n, const ConfigOption *opt_default = nullptr) = 0;
@@ -586,6 +589,32 @@ public:
throw ConfigurationError("ConfigOptionVector::set_only_diff(): Assigning an incompatible type");
}
//set a item related with extruder variants when apply static config with dynamic config
//rhs: item from dynamic config
//dest_index: which index in this vector need to be used
virtual void set_to_index(const ConfigOptionVectorBase* rhs, std::vector<int>& dest_index, int stride) override
{
if (rhs->type() == this->type()) {
// Assign the first value of the rhs vector.
auto other = static_cast<const ConfigOptionVector<T>*>(rhs);
T v = other->values.front();
this->values.resize(dest_index.size() * stride, v);
for (size_t i = 0; i < dest_index.size(); i++) {
if (dest_index[i] < 0)
continue;
for (size_t j = 0; j < size_t(stride); j++)
{
const size_t src_idx = size_t(dest_index[i]) * size_t(stride) + j;
if (src_idx < other->values.size() && !other->is_nil(size_t(dest_index[i]) * size_t(stride)))
this->values[i * size_t(stride) + j] = other->values[src_idx];
}
}
}
else
throw ConfigurationError("ConfigOptionVector::set_to_index(): Assigning an incompatible type");
}
//set a item related with extruder variants when saving user config, set the non-diff value of some extruder to nill
//this item has different value with inherit config
//rhs: item from userconfig
@@ -716,6 +745,7 @@ public:
return false;
}
// Apply an override option, possibly a nullable one.
//default_index are 0 based
bool apply_override(const ConfigOption *rhs, std::vector<int>& default_index) override {
if (this->nullable())
throw ConfigurationError("Cannot override a nullable ConfigOption.");
@@ -751,10 +781,14 @@ public:
this->values[i] = rhs_vec->values[i];
modified = true;
} else {
if ((i < default_index.size()) && (default_index[i] - 1 < default_value.size()))
this->values[i] = default_value[default_index[i] - 1];
// Orca: a negative slot (failed variant lookup) must not silently collapse the
// whole array to the first slot's value — the int-vs-size_t comparison used to
// promote -1 past the bounds check. Keep the slot's own value (get_at-style
// clamp) when no valid index is available.
if ((i < default_index.size()) && (default_index[i] >= 0) && (size_t(default_index[i]) < default_value.size()))
this->values[i] = default_value[default_index[i]];
else
this->values[i] = default_value[0];
this->values[i] = default_value[std::min(i, default_value.size() - 1)];
}
}
return modified;
@@ -2077,6 +2111,11 @@ public:
throw ConfigurationError("ConfigOptionEnumGeneric: Assigning an incompatible type");
// rhs could be of the following type: ConfigOptionEnumGeneric or ConfigOptionEnum<T>
this->value = rhs->getInt();
// Orca: options embedded in a StaticPrintConfig are constructed without a keys_map;
// adopt the source's so a later serialize() can emit names.
if (this->keys_map == nullptr)
if (auto rhs_generic = dynamic_cast<const ConfigOptionEnumGeneric *>(rhs))
this->keys_map = rhs_generic->keys_map;
}
std::string serialize() const override
@@ -2133,7 +2172,12 @@ public:
if (rhs->type() != this->type())
throw ConfigurationError("ConfigOptionEnumGeneric: Assigning an incompatible type");
// rhs could be of the following type: ConfigOptionEnumsGeneric
this->values = dynamic_cast<const ConfigOptionEnumsGenericTempl *>(rhs)->values;
auto rhs_enums = dynamic_cast<const ConfigOptionEnumsGenericTempl *>(rhs);
this->values = rhs_enums->values;
// Orca: options embedded in a StaticPrintConfig are constructed without a keys_map;
// adopt the source's so a later serialize() emits names instead of empty tokens.
if (this->keys_map == nullptr)
this->keys_map = rhs_enums->keys_map;
}
std::string serialize() const override
@@ -2226,6 +2270,11 @@ public:
legend,
// Vector value, but edited as a single string.
one_string,
plugin_picker,
// Raw JSON string value, edited through a dialog behind a button rather than in the row.
plugin_config,
// PrinterAgentChoice
printer_agent_select,
};
// Identifier of this option. It is stored here so that it is accessible through the by_serialization_key_ordinal map.
@@ -2442,6 +2491,13 @@ public:
// "serialized" - vector valued option is entered in a single edit field. Values are separated by a semicolon.
// "show_value" - even if enum_values / enum_labels are set, still display the value, not the enum label.
std::string gui_flags;
// Capability type of a plugin-backed option, e.g. "slicing-pipeline" / "printer-connection"
// (empty for ordinary options). GUIType::plugin_picker filters the plugin list by it, and it
// resolves the option's "plugins" manifest reference; see is_plugin_backed().
std::string plugin_type;
// Whether this option holds plugin capability name(s) that feed the "plugins" manifest -- true
// iff it declares a plugin_type. Setting plugin_type is the only step needed to add one.
bool is_plugin_backed() const { return !plugin_type.empty(); }
// Label of the GUI input field.
// In case the GUI input fields are grouped in some views, the label defines a short label of a grouped value,
// while full_label contains a label of a stand-alone field.
@@ -2755,14 +2811,29 @@ public:
//BBS: add json support
void save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const;
// Rebuild the in-memory "plugins" manifest (the "name;uuid;capability" references the plugin
// dispatchers consume) from the plugin-backed options via the registered resolver. save_to_json()
// derives the same manifest, but only when a preset is written to disk; a config assembled in
// memory for the backend (PresetBundle::full_config -> Print::apply) must refresh it here or a
// picked-but-unsaved plugin never resolves at slice/export time. No-op without a resolver.
void update_plugin_manifest();
// Set all the nullable values to nils.
void null_nullables();
static size_t load_from_gcode_string_legacy(ConfigBase& config, const char* str, ConfigSubstitutionContext& substitutions);
static void set_resolve_capability_fn(std::function<std::string(std::string, std::string)> fn) { resolve_capability_fn = fn; }
private:
// Set a configuration value from a string.
bool set_deserialize_raw(const t_config_option_key& opt_key_src, const std::string& value, ConfigSubstitutionContext& substitutions, bool append);
void save_plugin_collection(const std::string& opt_key, const ConfigOption* opt, std::vector<std::string>& plugin_refs) const;
// Collect the de-duplicated "name;uuid;capability" plugin references derived from this config's
// plugin-backed options via the resolver. Shared by save_to_json (serializes them into the JSON
// manifest) and update_plugin_manifest (writes them back into the "plugins" option). Order is
// preserved and empties are dropped; returns empty without a resolver (CLI/headless).
std::vector<std::string> collect_plugin_manifest() const;
static std::function<std::string(std::string, std::string)> resolve_capability_fn;
};
// Configuration store with dynamic number of configuration values.
@@ -2999,6 +3070,15 @@ protected:
void set_defaults();
};
struct PluginCapabilityRef
{
std::string name;
std::string capability_name;
std::string uuid;
};
std::optional<PluginCapabilityRef> parse_capability_ref(const std::string& value);
}
#endif
+27 -9
View File
@@ -13,11 +13,20 @@ Extruder::Extruder(unsigned int id, GCodeConfig *config, bool share_extruder) :
{
reset();
m_config_index = int(m_id);
// cache values that are going to be called often
m_e_per_mm3 = this->filament_flow_ratio();
m_e_per_mm3 /= this->filament_crossection();
}
void Extruder::set_config_index(int idx)
{
m_config_index = idx < 0 ? int(m_id) : idx;
// keep the cached flow term reading the same column as the getters
m_e_per_mm3 = this->filament_flow_ratio();
m_e_per_mm3 /= this->filament_crossection();
}
unsigned int Extruder::extruder_id() const
{
assert(m_config);
@@ -162,28 +171,35 @@ double Extruder::filament_cost() const
double Extruder::filament_flow_ratio() const
{
return m_config->filament_flow_ratio.get_at(m_id);
return m_config->filament_flow_ratio.get_at(m_config_index);
}
// Return a "retract_before_wipe" percentage as a factor clamped to <0, 1>
double Extruder::retract_before_wipe() const
{
return std::min(1., std::max(0., m_config->retract_before_wipe.get_at(m_id) * 0.01));
return std::clamp(m_config->retract_before_wipe.get_at(m_config_index) * 0.01, 0., 1.);
}
// Orca:
// Return a "retract_after_wipe" percentage as a factor clamped to <0, 1>
double Extruder::retract_after_wipe() const
{
return std::min(std::clamp(m_config->retract_after_wipe.get_at(m_config_index) * 0.01, 0., 1.), 1. - retract_before_wipe());
}
double Extruder::retraction_length() const
{
return m_config->retraction_length.get_at(m_id);
return m_config->retraction_length.get_at(m_config_index);
}
double Extruder::retract_lift() const
{
return m_config->z_hop.get_at(m_id);
return m_config->z_hop.get_at(m_config_index);
}
int Extruder::retract_speed() const
{
return int(floor(m_config->retraction_speed.get_at(m_id)+0.5));
return int(floor(m_config->retraction_speed.get_at(m_config_index)+0.5));
}
bool Extruder::use_firmware_retraction() const
@@ -193,27 +209,29 @@ bool Extruder::use_firmware_retraction() const
int Extruder::deretract_speed() const
{
int speed = int(floor(m_config->deretraction_speed.get_at(m_id)+0.5));
int speed = int(floor(m_config->deretraction_speed.get_at(m_config_index)+0.5));
return (speed > 0) ? speed : this->retract_speed();
}
double Extruder::retract_restart_extra() const
{
return m_config->retract_restart_extra.get_at(m_id);
return m_config->retract_restart_extra.get_at(m_config_index);
}
double Extruder::retract_length_toolchange() const
{
return m_config->retract_length_toolchange.get_at(extruder_id());
return m_config->retract_length_toolchange.get_at(m_config_index);
}
double Extruder::retract_restart_extra_toolchange() const
{
return m_config->retract_restart_extra_toolchange.get_at(extruder_id());
return m_config->retract_restart_extra_toolchange.get_at(m_config_index);
}
double Extruder::travel_slope() const
{
// Orca: deliberately keyed by the physical extruder, not the filament column — this read
// predates the per-variant merge and switching it would change existing multi-extruder output.
return m_config->travel_slope.get_at(extruder_id()) * PI / 180;
}
+15
View File
@@ -29,6 +29,13 @@ public:
unsigned int id() const { return m_id; }
// Column of the per-variant filament/override arrays the getters read. Defaults to the
// filament id (one column per filament); the g-code generator refreshes it on layer changes
// and toolchanges when a per-layer nozzle grouping gives a filament several variant columns.
int config_index() const { return m_config_index; }
// idx < 0 resets to the filament id. Re-syncs the cached e_per_mm3 flow term.
void set_config_index(int idx);
unsigned int extruder_id() const;
double extrude(double dE);
double retract(double length, double restart_extra);
@@ -51,6 +58,10 @@ public:
double retracted() const { return m_retracted; }
// Get extra retraction planned after
double restart_extra() const { return m_restart_extra; }
// Share-aware retracted-length readers (for extruders shared between filaments), consumed by GCodeWriter::get_extruder_retracted_length.
bool is_share_extruder() const { return m_share_extruder; }
double get_single_retracted_length() const { return m_retracted; }
double get_share_retracted_length() const { return m_share_retracted[extruder_id()]; }
// Setters for the PlaceholderParser.
// Set current extruder position. Only applicable with absolute extruder addressing.
void set_position(double e) { m_E = e; }
@@ -63,6 +74,8 @@ public:
double filament_cost() const;
double filament_flow_ratio() const;
double retract_before_wipe() const;
// Orca:
double retract_after_wipe() const;
double retraction_length() const;
double retract_lift() const;
int retract_speed() const;
@@ -82,6 +95,8 @@ private:
GCodeConfig *m_config;
// Print-wide global ID of this extruder.
unsigned int m_id;
// Column into the per-variant filament/override arrays; equals m_id unless refreshed.
int m_config_index{0};
// Current state of the extruder axis, may be resetted if use_relative_e_distances.
double m_E;
// Current state of the extruder tachometer, used to output the extruded_volume() and used_filament() statistics.
File diff suppressed because it is too large Load Diff
+112 -46
View File
@@ -13,7 +13,8 @@
const static int DEFAULT_CLUSTER_SIZE = 16;
const static int ABSOLUTE_FLUSH_GAP_TOLERANCE = 5;
const static int ABSOLUTE_FLUSH_GAP_TOLERANCE = 10;
namespace Slic3r
{
@@ -52,12 +53,12 @@ namespace Slic3r
struct MemoryedGroup {
MemoryedGroup() = default;
MemoryedGroup(const std::vector<int>& group_, const int cost_, const int prefer_level_) :group(group_), cost(cost_), prefer_level(prefer_level_) {}
MemoryedGroup(const std::vector<int>& group_, const double cost_, const int prefer_level_) :group(group_), cost(cost_), prefer_level(prefer_level_) {}
bool operator>(const MemoryedGroup& other) const {
return prefer_level < other.prefer_level || (prefer_level == other.prefer_level && cost > other.cost);
}
int cost{ 0 };
double cost{ 0 };
int prefer_level{ 0 };
std::vector<int>group;
};
@@ -75,6 +76,7 @@ namespace Slic3r
std::vector<FilamentGroupUtils::FilamentInfo> filament_info;
std::vector<std::string> filament_ids;
std::vector<std::set<int>> unprintable_filaments;
std::map<int, std::set<NozzleVolumeType>> unprintable_volumes;
} model_info;
struct GroupInfo {
@@ -82,40 +84,74 @@ namespace Slic3r
double max_gap_threshold;
FGMode mode;
FGStrategy strategy;
bool ignore_ext_filament; //wai gua filament
bool ignore_ext_filament;
bool has_filament_switcher = false;
std::vector<int> filament_volume_map;
} group_info;
struct MachineInfo {
std::vector<int> max_group_size;
std::vector<std::vector<FilamentGroupUtils::MachineFilamentInfo>> machine_filament_info;
std::vector<std::pair<std::set<int>, int>> extruder_group_size;
std::vector<bool> prefer_non_model_filament;
int master_extruder_id;
} machine_info;
struct SpeedInfo{
std::unordered_map<int,std::unordered_map<int,double>> filament_print_time;
double extruder_change_time;
double filament_change_time;
bool group_with_time;
MultiNozzleUtils::FilamentChangeTimeParams change_time_params;
std::vector<bool> ams_preload_enabled;
} speed_info;
struct NozzleInfo {
std::map<int, std::vector<int>> extruder_nozzle_list;
std::vector<MultiNozzleUtils::NozzleInfo> nozzle_list;
std::unordered_map<int, int> nozzle_status;
} nozzle_info;
};
std::vector<int> select_best_group_for_ams(const std::vector<std::vector<int>>& map_lists,
std::vector<int> select_best_group_for_ams(const std::vector<std::vector<int>> &filament_to_nozzles,
const std::vector<MultiNozzleUtils::NozzleInfo>& nozzle_list,
const std::vector<unsigned int>& used_filaments,
const std::vector<FilamentGroupUtils::FilamentInfo>& used_filament_info,
const std::vector<std::vector<FilamentGroupUtils::MachineFilamentInfo>>& machine_filament_info,
const bool has_filament_switcher = false,
const double color_delta_threshold = 20);
std::vector<int> optimize_group_for_master_extruder(const std::vector<unsigned int>& used_filaments, const FilamentGroupContext& ctx, const std::vector<int>& filament_map);
bool can_swap_groups(const int extruder_id_0, const std::set<int>& group_0, const int extruder_id_1, const std::set<int>& group_1, const FilamentGroupContext& ctx);
std::vector<int> calc_filament_group_for_tpu(const std::set<int>& tpu_filaments, const int filament_nums, const int master_extruder_id);
class FlushDistanceEvaluator
{
public:
FlushDistanceEvaluator(const FlushMatrix& flush_matrix,const std::vector<unsigned int>&used_filaments,const std::vector<std::vector<unsigned int>>& layer_filaments, double p = 0.65);
FlushDistanceEvaluator(const std::vector<FlushMatrix>& flush_matrix,const std::vector<unsigned int>&used_filaments,const std::vector<std::vector<unsigned int>>& layer_filaments, double p = 0.65);
~FlushDistanceEvaluator() = default;
double get_distance(int idx_a, int idx_b) const;
double get_distance(int idx_a, int idx_b, int extruder_id) const;
private:
std::vector<std::vector<float>>m_distance_matrix;
std::vector<std::vector<std::vector<float>>>m_distance_matrix;
};
class TimeEvaluator
{
public:
TimeEvaluator(const FilamentGroupContext::SpeedInfo& speed_info) : m_speed_info(speed_info) {}
double get_estimated_time(const std::vector<int>& filament_map) const;
private:
FilamentGroupContext::SpeedInfo m_speed_info;
};
// Search budget for the k-medoids clustering, an anytime search. Each restart is seeded from its
// own index, so what it returns depends on how many restarts complete before the clock expires,
// and therefore on the speed of the machine. A timeout_ms <= 0 removes the clock and bounds the
// search by max_restarts alone.
struct ClusteringBudget
{
int timeout_ms = 3000;
int max_restarts = 30;
};
class FilamentGroup
{
using MemoryedGroup = FilamentGroupUtils::MemoryedGroup;
@@ -123,17 +159,25 @@ namespace Slic3r
public:
explicit FilamentGroup(const FilamentGroupContext& ctx_) :ctx(ctx_) {}
public:
void set_clustering_budget(const ClusteringBudget& budget) { m_clustering_budget = budget; }
std::vector<int> calc_filament_group(int * cost = nullptr);
std::vector<std::vector<int>> get_memoryed_groups()const { return m_memoryed_groups; }
public:
std::vector<int> calc_filament_group_for_match(int* cost = nullptr);
std::vector<int> calc_filament_group_for_flush(int* cost = nullptr);
std::vector<int> calc_filament_group_for_tpu(int* cost = nullptr);
private:
std::vector<int> calc_min_flush_group(int* cost = nullptr);
std::vector<int> calc_min_flush_group_by_enum(const std::vector<unsigned int>& used_filaments, int* cost = nullptr);
std::vector<int> calc_min_flush_group_by_pam2(const std::vector<unsigned int>& used_filaments, int* cost = nullptr, int timeout_ms = 300);
std::vector<int> calc_group_by_enum(int k, const std::vector<unsigned int>& used_filaments,
const std::unordered_map<int, std::vector<int>>& unplaceable_limits, int* cost = nullptr);
std::vector<int> calc_group_by_kmedoids(int k, const std::vector<unsigned int>& used_filaments,
const std::unordered_map<int, std::vector<int>>& unplaceable_limits, int* cost = nullptr);
std::map<int, int> rebuild_unprintables(const std::vector<unsigned int>& used_filaments, const std::map<int,int>& extruder_unprintables);
std::unordered_map<int, std::vector<int>> rebuild_nozzle_unprintables(const std::vector<unsigned int>& used_filaments, const std::unordered_map<int, std::vector<int>>& extruder_unprintables, const std::vector<int>& filament_volume_map);
std::unordered_map<int, std::vector<int>> try_merge_filaments();
void rebuild_context(const std::unordered_map<int, std::vector<int>>& merged_filaments);
@@ -141,57 +185,79 @@ namespace Slic3r
private:
FilamentGroupContext ctx;
MemoryedGroupHeap m_memoryed_heap;
std::vector<std::vector<int>> m_memoryed_groups;
ClusteringBudget m_clustering_budget;
public:
std::optional<std::function<bool(int, std::vector<int>&)>> get_custom_seq;
};
class KMediods2
std::vector<int> calc_filament_group_for_manual_multi_nozzle(const std::vector<int>& filament_map_manual,const FilamentGroupContext& ctx);
std::vector<int> calc_filament_group_for_match_multi_nozzle(const FilamentGroupContext& ctx);
struct FilamentPlanRes
{
std::vector<int> fil_order;
std::vector<int> fil_nozzle_match;
};
std::vector<FilamentPlanRes> plan_filament_nozzle_mapping_and_order(const FilamentGroupContext& ctx);
class KMediods
{
protected:
using MemoryedGroupHeap = FilamentGroupUtils::MemoryedGroupHeap;
using MemoryedGroup = FilamentGroupUtils::MemoryedGroup;
enum INIT_TYPE
{
Random = 0,
Farthest
};
public:
KMediods2(const int elem_count, const std::shared_ptr<FlushDistanceEvaluator>& evaluator, int default_group_id = 0) :
m_evaluator{ evaluator },
m_elem_count{ elem_count },
m_default_group_id{ default_group_id }
{
m_max_cluster_size = std::vector<int>(m_k, DEFAULT_CLUSTER_SIZE);
KMediods(const int k, const int elem_count, const std::shared_ptr<FlushDistanceEvaluator>& evaluator, int default_group_id = 0) {
m_k = k;
m_evaluator = evaluator;
m_max_cluster_size = std::vector<int>(k, DEFAULT_CLUSTER_SIZE);
m_elem_count = elem_count;
m_default_group_id = default_group_id;
}
// set max group size
void set_max_cluster_size(const std::vector<int>& group_size) { m_max_cluster_size = group_size; }
// key stores elem idx, value stores the cluster id that elem cnanot be placed
void set_unplaceable_limits(const std::map<int, int>& placeable_limits) { m_unplaceable_limits = placeable_limits; }
void set_cluster_group_size(const std::vector<std::pair<std::set<int>,int>>& cluster_group_size);
void do_clustering(const FGStrategy& g_strategy,int timeout_ms = 100);
// key stores elem, value stores the cluster id that the elem must be placed
void set_placable_limits(const std::unordered_map<int, std::vector<int>>& placable_limits) { m_placeable_limits = placable_limits; }
// key stores elem, value stores the cluster id that the elem cannot be placed
void set_unplacable_limits(const std::unordered_map<int, std::vector<int>>& unplacable_limits) { m_unplaceable_limits = unplacable_limits; }
void set_memory_threshold(double threshold) { memory_threshold = threshold; }
MemoryedGroupHeap get_memoryed_groups()const { return memoryed_groups; }
std::vector<int>get_cluster_labels()const { return m_cluster_labels; }
void do_clustering(const FilamentGroupContext& context, const ClusteringBudget& budget);
std::vector<int> get_cluster_labels()const { return m_cluster_labels; }
private:
std::vector<int>cluster_small_data(const std::map<int, int>& unplaceable_limits, const std::vector<int>& group_size);
std::vector<int>assign_cluster_label(const std::vector<int>& center, const std::map<int, int>& unplaceable_limits, const std::vector<int>& group_size, const FGStrategy& strategy);
int calc_cost(const std::vector<int>& labels, const std::vector<int>& medoids);
protected:
FilamentGroupUtils::MemoryedGroupHeap memoryed_groups;
std::shared_ptr<FlushDistanceEvaluator> m_evaluator;
std::map<int, int>m_unplaceable_limits;
std::vector<int>m_cluster_labels;
std::vector<int>m_max_cluster_size;
bool have_enough_size(const std::vector<int>& cluster_size, const std::vector<std::pair<std::set<int>, int>>& cluster_group_size,int elem_count);
// calculate cluster distance
int calc_cost(const std::vector<int>& clusters, const std::vector<int>& cluster_centers, int cluster_id = -1);
const int m_k = 2;
// get initial cluster center
std::vector<int>init_cluster_center(const std::unordered_map<int, std::vector<int>>& placeable_limits, const std::unordered_map<int, std::vector<int>>& unplaceable_limits, const std::vector<int>& cluster_size, const std::vector<std::pair<std::set<int>, int>>& cluster_group_size, int seed);
// assign each elem to the cluster
std::vector<int> assign_cluster_label(const std::vector<int>& center, const std::unordered_map<int, std::vector<int>>& placeable_limits, const std::unordered_map<int, std::vector<int>>& unplaceable_limits, const std::vector<int>& group_size, const std::vector<std::pair<std::set<int>, int>>& cluster_group_size);
protected:
MemoryedGroupHeap memoryed_groups;
std::shared_ptr<FlushDistanceEvaluator>m_evaluator;
std::unordered_map<int, std::vector<int>> m_unplaceable_limits; // key: filament, value: nozzle ids it cannot be assigned to
std::unordered_map<int, std::vector<int>> m_placeable_limits; // key: filament, value: nozzle ids it must be assigned to
std::vector<int>m_max_cluster_size; // max number of filaments each nozzle can hold
std::vector<int>m_cluster_labels; // assignment result, resolved down to nozzle id
std::vector<std::pair<std::set<int>,int>> m_cluster_group_size;
std::vector<int> m_nozzle_to_extruder;
int m_k;
int m_elem_count;
int m_default_group_id{ 0 };
double memory_threshold{ 0 };
+65
View File
@@ -274,5 +274,70 @@ namespace FilamentGroupUtils
}
return true;
}
int get_estimate_extruder_change_count(const std::vector<std::vector<unsigned int>> &layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult &extruder_nozzle_info)
{
int ret = 0;
for (size_t layer_id = 0; layer_id < layer_filaments.size(); ++layer_id) {
int extruder_count = extruder_nozzle_info.get_used_extruders(layer_id).size();
ret += (extruder_count - 1);
}
return ret;
}
int get_estimate_nozzle_change_count(const std::vector<std::vector<unsigned int>> &layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult &extruder_nozzle_info)
{
int ret = 0;
for (size_t layer_id = 0; layer_id < layer_filaments.size(); ++layer_id) {
auto extruder_list = extruder_nozzle_info.get_used_extruders(layer_id);
for (auto extruder_id : extruder_list) {
int nozzle_count = extruder_nozzle_info.get_used_nozzles_in_extruder(extruder_id, layer_id).size();
if (nozzle_count > 1) ret += (nozzle_count - 1);
}
}
return ret;
}
std::pair<int, int> get_estimate_extruder_filament_change_count(const MultiNozzleUtils::LayeredNozzleGroupResult &extruder_nozzle_info)
{
std::pair<int, int> ret{0,0};
int layer_nums = extruder_nozzle_info.get_layer_filament_sequences().size();
for (int layer_id = 0; layer_id < layer_nums; layer_id++) {
std::vector<int> extruders = extruder_nozzle_info.get_used_extruders(layer_id);
ret.first = extruders.size() - 1;
for (auto ext_id : extruders) {
int nozzles = extruder_nozzle_info.get_used_nozzles_in_extruder(ext_id, layer_id).size();
ret.second += nozzles;
}
ret.second = std::max(0, ret.second - ret.first);
}
return ret;
}
std::map<int,std::vector<int>> build_extruder_nozzle_list(const std::vector<MultiNozzleUtils::NozzleInfo>& nozzle_list)
{
std::map<int, std::vector<int>> ret;
for (auto& nozzle : nozzle_list) {
ret[nozzle.extruder_id].emplace_back(nozzle.group_id);
}
for (auto& elem : ret)
std::sort(elem.second.begin(), elem.second.end());
return ret;
}
std::vector<int> update_used_filament_values(const std::vector<int>& old_values, const std::vector<int>& new_values, const std::vector<unsigned int>& used_filaments)
{
std::vector<int> res = old_values;
for (size_t i = 0; i < used_filaments.size(); ++i) {
// Orca: guard against filament ids beyond the map sizes (possible with
// mis-normalized per-filament arrays from CLI inputs); skip instead of UB.
if (used_filaments[i] >= res.size() || used_filaments[i] >= new_values.size())
continue;
res[used_filaments[i]] = new_values[used_filaments[i]];
}
return res;
}
}
}
+19
View File
@@ -7,6 +7,7 @@
#include <exception>
#include "PrintConfig.hpp"
#include "MultiNozzleUtils.hpp"
namespace Slic3r
{
@@ -31,6 +32,10 @@ namespace Slic3r
Color color;
std::string type;
bool is_support;
// How this filament is used across the model. Orca's shipping grouping
// algorithm does not read it yet; defaulted so a default-built FilamentInfo
// is deterministic. The nozzle-centric engine consumes it later.
FilamentUsageType usage_type = FilamentUsageType::ModelOnly;
};
struct MachineFilamentInfo: public FilamentInfo {
@@ -80,6 +85,20 @@ namespace Slic3r
void extract_unprintable_limit_indices(const std::vector<std::set<int>>& unprintable_elems, const std::vector<unsigned int>& used_filaments, std::unordered_map<int, std::vector<int>>& unplaceable_limits);
bool check_printable(const std::vector<std::set<int>>& groups, const std::map<int, int>& unprintable);
// Nozzle-centric grouping helpers. The estimate helpers read a LayeredNozzleGroupResult's
// per-layer extruder/nozzle usage; the two builders support building the grouping context
// (extruder->nozzle inventory) and writing back a resolved map onto only the used-filament
// slots.
int get_estimate_extruder_change_count(const std::vector<std::vector<unsigned int>>& layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult& extruder_nozzle_info);
int get_estimate_nozzle_change_count(const std::vector<std::vector<unsigned int>>& layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult& extruder_nozzle_info);
std::pair<int, int> get_estimate_extruder_filament_change_count(const MultiNozzleUtils::LayeredNozzleGroupResult& extruder_nozzle_info);
std::map<int, std::vector<int>> build_extruder_nozzle_list(const std::vector<MultiNozzleUtils::NozzleInfo>& nozzle_list);
std::vector<int> update_used_filament_values(const std::vector<int>& old_values, const std::vector<int>& new_values, const std::vector<unsigned int>& used_filaments);
}
+90 -4
View File
@@ -57,6 +57,9 @@ double calculate_infill_rotation_angle(const PrintObject* object,
if (template_string.empty()) {
return Geometry::deg2rad(fixed_infill_angle);
}
// Convert the id to an index. Layer::id() counts the raft layers, object->layers() does not.
const size_t first_object_layer_id = object->get_layer(0)->id();
layer_id = layer_id > first_object_layer_id ? layer_id - first_object_layer_id : 0;
double angle = 0.0;
ConfigOptionFloats rotate_angles;
const std::string search_string = "/NnZz$LlUuQq~^|#";
@@ -75,6 +78,8 @@ double calculate_infill_rotation_angle(const PrintObject* object,
double angle_start = 0;
double limit_fill_z = object->get_layer(0)->bottom_z();
double start_fill_z = limit_fill_z;
// The raft height, or 0 without a raft.
const double print_z_offset = object->slicing_parameters().object_print_z_min;
bool _noop = false;
auto fill_form = std::string::npos;
bool _absolute = false;
@@ -84,7 +89,8 @@ double calculate_infill_rotation_angle(const PrintObject* object,
for (int i = 0; i <= layer_id; i++) {
double fill_z = object->get_layer(i)->bottom_z();
if (limit_fill_z < object->get_layer(i)->slice_z) {
// slice_z is measured from the bottom of the model, limit_fill_z from the build plate.
if (limit_fill_z < object->get_layer(i)->slice_z + print_z_offset) {
if (repeats) { // if repeats >0 then restore parameters for new iteration
limit_fill_z += limit_fill_z - start_fill_z;
start_fill_z = fill_z;
@@ -272,6 +278,15 @@ struct SurfaceFillParams
// For Gyroid: when true, use the parameterized "optimized" wave.
bool gyroid_optimized = false;
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
CenterOfSurfacePattern center_of_surface_pattern{CenterOfSurfacePattern::Each_Surface};
bool separated_infills{false};
// Orca: forced print order of surface fill loops/fragments for center-based patterns.
SurfaceFillOrder fill_order = SurfaceFillOrder::Default;
bool operator<(const SurfaceFillParams &rhs) const {
#define RETURN_COMPARE_NON_EQUAL(KEY) if (this->KEY < rhs.KEY) return true; if (this->KEY > rhs.KEY) return false;
#define RETURN_COMPARE_NON_EQUAL_TYPED(TYPE, KEY) if (TYPE(this->KEY) < TYPE(rhs.KEY)) return true; if (TYPE(this->KEY) > TYPE(rhs.KEY)) return false;
@@ -301,8 +316,13 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(lateral_lattice_angle_2);
RETURN_COMPARE_NON_EQUAL(symmetric_infill_y_axis);
RETURN_COMPARE_NON_EQUAL(infill_lock_depth);
RETURN_COMPARE_NON_EQUAL(skin_infill_depth); RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
RETURN_COMPARE_NON_EQUAL(smooth_factor);
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
RETURN_COMPARE_NON_EQUAL(separated_infills);
RETURN_COMPARE_NON_EQUAL_TYPED(unsigned, fill_order);
return false;
}
@@ -329,7 +349,11 @@ struct SurfaceFillParams
this->infill_lock_depth == rhs.infill_lock_depth &&
this->skin_infill_depth == rhs.skin_infill_depth &&
this->infill_overhang_angle == rhs.infill_overhang_angle &&
this->gyroid_optimized == rhs.gyroid_optimized;
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
this->separated_infills == rhs.separated_infills &&
this->gyroid_optimized == rhs.gyroid_optimized &&
this->smooth_factor == rhs.smooth_factor &&
this->fill_order == rhs.fill_order;
}
};
@@ -868,6 +892,8 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.lateral_lattice_angle_1 = region_config.lateral_lattice_angle_1;
params.lateral_lattice_angle_2 = region_config.lateral_lattice_angle_2;
params.infill_overhang_angle = region_config.infill_overhang_angle;
params.center_of_surface_pattern = region_config.center_of_surface_pattern;
params.separated_infills = region_config.separated_infills;
if (params.pattern == ipLockedZag) {
params.infill_lock_depth = scale_(region_config.infill_lock_depth);
params.skin_infill_depth = scale_(region_config.skin_infill_depth);
@@ -922,6 +948,14 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.extruder = region_config.bottom_surface_filament_id;
else if (params.extrusion_role == erSolidInfill)
params.extruder = region_config.internal_solid_filament_id;
// Orca: forced fill order applies only to top/bottom surfaces filled with a
// center-based pattern; everything else stays at Default to keep batching together.
if (params.pattern == ipConcentric || params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral) {
if (params.extrusion_role == erTopSolidInfill)
params.fill_order = region_config.top_surface_fill_order.value;
else if (params.extrusion_role == erBottomSurface)
params.fill_order = region_config.bottom_surface_fill_order.value;
}
// Orca: apply fill multiline only for sparse infill
params.multiline = params.extrusion_role == erInternalInfill ? int(region_config.fill_multiline) : 1;
@@ -935,6 +969,11 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
region_config.sparse_infill_rotate_template.value);
params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty();
// Orca: special case; apply smoothing factor only for Hilbert Curve sparse infill.
// FillHilbertCurve::generate clamps and validates the value itself.
if (params.pattern == ipHilbertCurve)
params.smooth_factor = 0.01 * region_config.sparse_infill_smooth_factor.value;
} else {
const bool top_layer_direction_set = surface.is_top() && region_config.top_layer_direction.value >= 0.;
const bool bottom_layer_direction_set = surface.is_bottom() && region_config.bottom_layer_direction.value >= 0.;
@@ -1299,6 +1338,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.smooth_factor = surface_fill.params.smooth_factor;
// BBS
params.flow = surface_fill.params.flow;
@@ -1308,6 +1348,22 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
auto &region_config = layerm->region().config();
params.config = &region_config;
params.pattern = surface_fill.params.pattern;
params.fill_order = surface_fill.params.fill_order;
// Orca: Checking the filling of a centered surface by drawing for each model parts
bool is_top_or_bottom = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface;
bool is_centered_infill = surface_fill.params.pattern == ipArchimedeanChords || surface_fill.params.pattern == ipOctagramSpiral;
if (is_top_or_bottom) {
params.center_of_surface_pattern = surface_fill.params.center_of_surface_pattern; // Orca: center of surface pattern
}
// Orca: Each_Model centers the pattern on each model part's bbox; Each_Surface / Each_Assembly
// fall through to the default (whole-object) bounding box below.
bool is_per_model_center = is_top_or_bottom && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model && is_centered_infill;
bool is_separate_infill = !is_top_or_bottom && surface_fill.params.separated_infills &&
(
is_separable_infill_pattern(surface_fill.params.pattern) ||
params.config->solid_infill_rotate_template != "" ||
params.config->sparse_infill_rotate_template != "" );
if( surface_fill.params.pattern == ipLockedZag ) {
params.locked_zag = true;
@@ -1332,7 +1388,36 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
params.can_reverse = false;
for (ExPolygon& expoly : surface_fill.expolygons) {
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
// Orca: separate infill / per-model pattern centering.
//
// Center the pattern on each connected body of the object independently, so every piece
// is filled exactly as if it were sliced on its own: touching/overlapping parts merge
// into one body sharing a center, while separate parts and disconnected islands (even
// interleaved-but-not-touching ones, e.g. chain links) each get their own. The body each
// island belongs to, and its full bounding box, were resolved in 3D by PrintObject::
// infill() (lslices_separated_component_bboxes, aligned with this layer's lslices). We
// match this fill region to the island it overlaps most, then re-use the whole-object
// bounding box (origin-centered — identical extent to the default, so coverage and cost
// are unchanged) re-centered on that body.
if (is_per_model_center || is_separate_infill) {
double best_overlap = 0.;
BoundingBox best_component;
for (size_t r = 0; r < this->lslices.size() && r < this->lslices_separated_component_bboxes.size(); ++ r) {
const double overlap = area(intersection_ex(this->lslices[r], expoly));
if (overlap > best_overlap) {
best_overlap = overlap;
best_component = this->lslices_separated_component_bboxes[r];
}
}
if (best_component.defined) {
const Point c = best_component.center();
BoundingBox part_bbox = bbox; // origin-centered, whole-object extent (from above)
part_bbox.translate(c.x(), c.y()); // re-center on this body
f->set_bounding_box(part_bbox);
}
} // - End: separate infill / per-model pattern centering
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
if (params.symmetric_infill_y_axis) {
params.symmetric_y_axis = f->extended_object_bounding_box().center().x();
expoly.symmetric_y(params.symmetric_y_axis);
@@ -1495,6 +1580,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.multiline = surface_fill.params.multiline;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.smooth_factor = surface_fill.params.smooth_factor;
for (ExPolygon &expoly : surface_fill.expolygons) {
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
+11 -7
View File
@@ -165,7 +165,11 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
// ORCA: special flag for flow rate calibration
auto is_flow_calib = params.extrusion_role == erTopSolidInfill && this->print_object_config->has("calib_flowrate_topinfill_special_order") &&
this->print_object_config->option("calib_flowrate_topinfill_special_order")->getBool();
if (is_flow_calib) {
// Orca: a forced surface fill order must survive the G-code path planner, which would
// otherwise re-chain and possibly reverse the paths. The same applies to the flow rate
// calibration's special toolpath order.
const bool keep_fill_order = params.fill_order != SurfaceFillOrder::Default;
if (is_flow_calib || keep_fill_order) {
eec->no_sort = true;
}
size_t idx = eec->entities.size();
@@ -180,7 +184,7 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
params.extrusion_role,
flow_mm3_per_mm, float(flow_width), params.flow.height());
}
if (!params.can_reverse || is_flow_calib) {
if (!params.can_reverse || is_flow_calib || keep_fill_order) {
for (size_t i = idx; i < eec->entities.size(); i++)
eec->entities[i]->set_reverse();
}
@@ -1853,12 +1857,12 @@ static inline void base_support_extend_infill_lines(Polylines &infill, BoundaryI
const bool first = graph.first(cp);
int extend_next_idx = -1;
int extend_prev_idx = -1;
coord_t dist_y_prev;
coord_t dist_y_next;
double arc_len_prev;
double arc_len_next;
coord_t dist_y_prev = 0;
coord_t dist_y_next = 0;
double arc_len_prev = 0;
double arc_len_next = 0;
if (! graph.next_vertical(cp)){
if (! graph.next_vertical(cp)) {
size_t i = cp.point_idx;
size_t j = next_idx_modulo(i, contour);
while (j != cp.next_on_contour->point_idx) {
+8
View File
@@ -82,6 +82,9 @@ struct FillParams
// For Gyroid: when true, use the parameterized "optimized" variant.
bool gyroid_optimized { false };
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
// For Lateral lattice
coordf_t lateral_lattice_angle_1 { 0.f };
coordf_t lateral_lattice_angle_2 { 0.f };
@@ -100,12 +103,17 @@ struct FillParams
bool dont_sort{ false }; // do not sort the lines, just simply connect them
bool can_reverse{true};
// Orca: forced print order of surface fill loops/fragments for center-based patterns
// (Concentric, Archimedean Chords, Octagram Spiral). Default keeps shortest-path ordering.
SurfaceFillOrder fill_order { SurfaceFillOrder::Default };
float horiz_move{0.0}; //move infill to get cross zag pattern
bool symmetric_infill_y_axis{false};
coord_t symmetric_y_axis{0};
bool locked_zag{false};
float infill_lock_depth{0.0};
float skin_infill_depth{0.0};
CenterOfSurfacePattern center_of_surface_pattern{CenterOfSurfacePattern::Each_Surface};
};
static_assert(IsTriviallyCopyable<FillParams>::value, "FillParams class is not POD (and it should be - see constructor).");
+17 -1
View File
@@ -41,6 +41,10 @@ void FillConcentric::_fill_surface_single(
// generate paths from the outermost to the innermost, to avoid
// adhesion problems of the first central tiny loops
loops = union_pt_chained_outside_in(loops);
// Orca: an outward fill order prints the innermost loops first instead.
if (params.fill_order == SurfaceFillOrder::Outward)
std::reverse(loops.begin(), loops.end());
// split paths using a nearest neighbor search
size_t iPathFirst = polylines_out.size();
@@ -108,6 +112,17 @@ void FillConcentric::_fill_surface_single(const FillParams& params,
all_extrusions.emplace_back(&wall);
}
// Orca: a forced fill order prints the loops in strictly monotonic depth order so
// that surfaces broken up by holes or slots cannot hop outward and back inward.
const bool forced_fill_order = params.fill_order != SurfaceFillOrder::Default;
if (forced_fill_order) {
const bool outward = params.fill_order == SurfaceFillOrder::Outward;
std::stable_sort(all_extrusions.begin(), all_extrusions.end(),
[outward](const Arachne::ExtrusionLine *a, const Arachne::ExtrusionLine *b) {
return outward ? a->inset_idx > b->inset_idx : a->inset_idx < b->inset_idx;
});
}
// Split paths using a nearest neighbor search.
size_t firts_poly_idx = thick_polylines_out.size();
Point last_pos(0, 0);
@@ -136,7 +151,8 @@ void FillConcentric::_fill_surface_single(const FillParams& params,
if (j < thick_polylines_out.size())
thick_polylines_out.erase(thick_polylines_out.begin() + int(j), thick_polylines_out.end());
reorder_by_shortest_traverse(thick_polylines_out);
if (!forced_fill_order)
reorder_by_shortest_traverse(thick_polylines_out);
}
else {
Polylines polylines;
+197 -16
View File
@@ -77,20 +77,24 @@ void FillPlanePath::_fill_surface_single(
//FIXME Vojtech: We are not sure whether the user expects the fill patterns on visible surfaces to be aligned across all the islands of a single layer.
// One may align for this->centered() to align the patterns for Archimedean Chords and Octagram Spiral patterns.
const bool align = params.density < 0.995;
// Orca: the old implementation became obsolete when it became possible to change the density of the top and bottom surfaces
bool align = params.extrusion_role == ExtrusionRole::erInternalInfill;
BoundingBox bounding_box;
BoundingBox snug_bounding_box = get_extents(expolygon).inflated(SCALED_EPSILON);
// Expand the bounding box to avoid artifacts at the edges
snug_bounding_box.offset(scale_(this->spacing)*params.multiline);
snug_bounding_box.offset(scale_(this->spacing)*params.multiline);
// Rotated bounding box of the area to fill in with the pattern.
BoundingBox bounding_box = align ?
// Sparse infill needs to be aligned across layers. Align infill across layers using the object's bounding box.
this->bounding_box.rotated(-direction.first) :
// Solid infill does not need to be aligned across layers, generate the infill pattern
// around the clipping expolygon only.
snug_bounding_box;
// Sparse infill (or Internal where align == true) needs to be aligned across layers. Align infill across layers using the object's bounding box.
// Solid infill does not need to be aligned across layers, generate the infill pattern around the clipping expolygon only.
if (align)
bounding_box = this->bounding_box.rotated(-direction.first);
else if (params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Surface)
bounding_box = snug_bounding_box;
else if (params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model)
bounding_box = this->bounding_box.rotated(-direction.first);
else
bounding_box = extended_object_bounding_box();
Point shift = this->centered() ?
bounding_box.center() :
@@ -110,12 +114,12 @@ void FillPlanePath::_fill_surface_single(
// Filling in a bounding box over the whole object, clip generated polyline against the snug bounding box.
snug_bounding_box.translate(-shift.x(), -shift.y());
InfillPolylineClipper output(snug_bounding_box, distance_between_lines);
this->generate(min_x, min_y, max_x, max_y, resolution, output);
this->generate(min_x, min_y, max_x, max_y, resolution, params, output);
polyline.points = std::move(output.result());
} else {
// Filling in a snug bounding box, no need to clip.
InfillPolylineOutput output(distance_between_lines);
this->generate(min_x, min_y, max_x, max_y, resolution, output);
this->generate(min_x, min_y, max_x, max_y, resolution, params, output);
polyline.points = std::move(output.result());
}
}
@@ -130,8 +134,12 @@ void FillPlanePath::_fill_surface_single(
if (!polylines.empty()) {
Polylines chained;
if (params.dont_connect() || params.density > 0.5) {
// ORCA: special flag for flow rate calibration
auto is_flow_calib = params.extrusion_role == erTopSolidInfill &&
// ORCA: special flag for flow rate calibration. The chords chained ahead of the
// inside-out center spiral collide with it in opposing directions, raising a
// tactile lip that the calibration reads. Only applies while the fill order is
// Default, so it can be overridden from the calibration objects.
auto is_flow_calib = params.fill_order == SurfaceFillOrder::Default &&
params.extrusion_role == erTopSolidInfill &&
this->print_object_config->has("calib_flowrate_topinfill_special_order") &&
this->print_object_config->option("calib_flowrate_topinfill_special_order")->getBool() &&
dynamic_cast<FillArchimedeanChords*>(this);
@@ -149,12 +157,26 @@ void FillPlanePath::_fill_surface_single(
// Chain the other polylines
polylines.erase(it);
chained = chain_polylines(std::move(polylines));
chained = chain_polylines(std::move(polylines), nullptr);
// Then add the center spiral back
chained.push_back(std::move(center_spiral));
} else if (params.fill_order != SurfaceFillOrder::Default) {
// Orca: print the fragments in the order they appear along the generated
// path, which runs from the center outwards. The Euclidean distance from
// the center cannot be used for this: along the Octagram Spiral the radius
// oscillates by far more than the ring spacing, so fragments of different
// rings would interleave.
restore_source_path_order(polyline, polylines);
chained = std::move(polylines);
if (params.fill_order == SurfaceFillOrder::Inward) {
// The source path runs from the center outwards; flip everything for inward.
std::reverse(chained.begin(), chained.end());
for (Polyline &pl : chained)
pl.reverse();
}
} else {
chained = chain_polylines(std::move(polylines));
chained = chain_polylines(std::move(polylines), nullptr);
}
} else
connect_infill(std::move(polylines), expolygon, chained, this->spacing, params);
@@ -266,6 +288,147 @@ static void generate_hilbert_curve(coord_t min_x, coord_t min_y, coord_t max_x,
}
}
using QuinticBezier = std::array<Vec2d, 6>;
static bool is_bezier_flat(const QuinticBezier &curve, const double deviation)
{
// A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every
// control point within a deviation-wide strip around the endpoint chord conservatively bounds the
// flattening error. The cross product is the perpendicular distance scaled by the chord length;
// comparing squared values avoids a square root.
const Vec2d chord = curve.back() - curve.front();
const double chord_length_sq = chord.squaredNorm();
const double max_cross_sq = deviation * deviation * chord_length_sq;
for (size_t i = 1; i + 1 < curve.size(); ++i) {
const Vec2d offset = curve[i] - curve.front();
const double cross = chord.x() * offset.y() - chord.y() * offset.x();
if (cross * cross > max_cross_sq)
return false;
}
return true;
}
static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right)
{
// Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one
// control point to the left half and one to the right half; the latter is filled backwards to keep
// both resulting control polygons in their original parameter direction.
QuinticBezier subdivision = curve;
left.front() = subdivision.front();
right.back() = subdivision.back();
for (size_t level = 1; level < curve.size(); ++level) {
for (size_t i = 0; i + level < curve.size(); ++i)
subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]);
left[level] = subdivision.front();
right[curve.size() - level - 1] = subdivision[curve.size() - level - 1];
}
}
static void flatten_bezier(const QuinticBezier &curve, const double deviation, std::vector<Vec2d> &output)
{
// Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord.
// A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth,
// avoiding abrupt segment-length jumps at adaptive-depth boundaries.
static constexpr size_t max_depth = 16;
std::vector<QuinticBezier> subcurves(2);
subdivide_bezier(curve, subcurves[0], subcurves[1]);
for (size_t depth = 1; depth < max_depth; ++depth) {
bool all_flat = true;
for (const QuinticBezier &c : subcurves)
if (!is_bezier_flat(c, deviation)) {
all_flat = false;
break;
}
if (all_flat)
break;
std::vector<QuinticBezier> finer(subcurves.size() * 2);
for (size_t i = 0; i < subcurves.size(); ++i)
subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]);
subcurves = std::move(finer);
}
// The curve start is deliberately omitted so consecutive curve pieces can share it without duplication.
output.reserve(output.size() + subcurves.size());
for (const QuinticBezier &c : subcurves)
output.emplace_back(c.back());
}
template<typename Output>
static void generate_smooth_hilbert_curve(
coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
const double corner_distance, Output &output)
{
// A Hilbert curve is defined on a square grid whose side is a power of two. As in the unsmoothed
// generator, expand the larger requested dimension to the next valid Hilbert grid size. The output
// clipper or the later region intersection removes the padded part of the traversal.
size_t sz = 2;
const size_t sz0 = std::max(max_x + 1 - min_x, max_y + 1 - min_y);
while (sz < sz0)
sz <<= 1;
const size_t point_count = sz * sz;
output.reserve(point_count);
// The caller normalizes resolution to the unit Hilbert grid; retain a finite positive tolerance
// if this helper is invoked with an invalid resolution.
const double deviation = resolution > 0. && std::isfinite(resolution) ? resolution : EPSILON;
// Construct one canonical 90-degree corner from (-corner_distance, 0) to (0, corner_distance).
// At each end, the first three control points are collinear and equally spaced: the tangent follows
// the adjoining straight leg and the second derivative is zero. The endpoint curvature is therefore
// zero, giving G2 joins to both legs. Every Hilbert turn is an oriented copy of this curve, so flatten
// it only once to the requested chordal-deviation tolerance.
const QuinticBezier corner_curve {{
{-corner_distance, 0.}, {-0.7 * corner_distance, 0.}, {-0.4 * corner_distance, 0.},
{0., 0.4 * corner_distance}, {0., 0.7 * corner_distance}, {0., corner_distance}
}};
std::vector<Vec2d> curve_coefficients;
flatten_bezier(corner_curve, deviation, curve_coefficients);
auto translated_point = [min_x, min_y](size_t idx) {
Point p = hilbert_n_to_xy(idx);
return Point(p.x() + min_x, p.y() + min_y);
};
auto to_vec2d = [](const Point &p) { return Vec2d(double(p.x()), double(p.y())); };
bool has_last_output = false;
Vec2d last_output;
// Fully smoothed adjacent corners may meet at the same segment midpoint. Suppress such duplicates
// to avoid emitting zero-length extrusion segments.
auto add_point = [&output, &has_last_output, &last_output](const Vec2d &point) {
if (!has_last_output || point.x() != last_output.x() || point.y() != last_output.y()) {
output.add_point(point);
last_output = point;
has_last_output = true;
}
};
Vec2d previous = to_vec2d(translated_point(0));
Vec2d corner = to_vec2d(translated_point(1));
add_point(previous);
// Replace each non-collinear Hilbert vertex by the canonical curve expressed in the local basis of
// its incoming and outgoing unit vectors. Collinear vertices remain part of the straight polyline.
for (size_t i = 1; i + 1 < point_count; ++i) {
const Vec2d next = to_vec2d(translated_point(i + 1));
const Vec2d incoming = (corner - previous).normalized();
const Vec2d outgoing = (next - corner).normalized();
const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x();
if (std::abs(cross) < EPSILON) {
add_point(corner);
} else {
add_point(corner - corner_distance * incoming);
for (const Vec2d &coefficient : curve_coefficients)
add_point(corner + coefficient.x() * incoming + coefficient.y() * outgoing);
}
previous = corner;
corner = next;
}
add_point(corner);
}
void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double /* resolution */, InfillPolylineOutput &output)
{
if (output.clips())
@@ -274,6 +437,24 @@ void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coo
generate_hilbert_curve(min_x, min_y, max_x, max_y, output);
}
void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
const FillParams &params, InfillPolylineOutput &output)
{
const double smooth_factor = std::isfinite(params.smooth_factor) ?
std::clamp(params.smooth_factor, 0., 1.) : 0.;
if (smooth_factor == 0.) {
this->generate(min_x, min_y, max_x, max_y, resolution, output);
return;
}
const double corner_distance = 0.5 * smooth_factor;
if (output.clips())
generate_smooth_hilbert_curve(
min_x, min_y, max_x, max_y, resolution, corner_distance, static_cast<InfillPolylineClipper&>(output));
else
generate_smooth_hilbert_curve(min_x, min_y, max_x, max_y, resolution, corner_distance, output);
}
template<typename Output>
static void generate_octagram_spiral(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, Output &output)
{
+7
View File
@@ -53,6 +53,11 @@ protected:
friend class InfillPolylineClipper;
virtual void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) = 0;
virtual void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
const FillParams & /* params */, InfillPolylineOutput &output)
{
this->generate(min_x, min_y, max_x, max_y, resolution, output);
}
};
class FillArchimedeanChords : public FillPlanePath
@@ -75,6 +80,8 @@ public:
protected:
bool centered() const override { return false; }
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) override;
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
const FillParams &params, InfillPolylineOutput &output) override;
};
class FillOctagramSpiral : public FillPlanePath
+32 -9
View File
@@ -2739,13 +2739,19 @@ static void polylines_from_paths(const std::vector<MonotonicRegionLink> &path, c
// The extended bounding box of the whole object that covers any rotation of every layer.
BoundingBox FillRectilinear::extended_object_bounding_box() const {
// Build the extension around the box center. The transpose merge and the sqrt(2.) scaling
// (which covers any possible rotation) are both defined about the origin, so a box that is not
// origin-centered — e.g. a separated-infill box re-centered on a single assembly part — would be
// distorted. Shift to the origin first and back afterwards; for the default origin-centered box
// the two translations cancel and this is identical to the original behavior.
const Point c = this->bounding_box.center();
BoundingBox out = this->bounding_box;
out.translate(-c.x(), -c.y());
out.merge(Point(out.min.y(), out.min.x()));
out.merge(Point(out.max.y(), out.max.x()));
// The bounding box is scaled by sqrt(2.) to ensure that the bounding box
// covers any possible rotations.
return out.scaled(sqrt(2.));
out = out.scaled(sqrt(2.));
out.translate(c.x(), c.y());
return out;
}
bool FillRectilinear::fill_surface_by_lines(const Surface *surface, const FillParams &params, float angleBase, float pattern_shift, Polylines &polylines_out)
@@ -3098,8 +3104,11 @@ bool FillRectilinear::fill_surface_trapezoidal(
const coord_t d2 = coord_t(0.5 * period - d1);
// Align bounding box to the grid
bb.merge(align_to_grid(bb.min, Point(period, period)));
// Align bounding box to the grid, phased through the box center so separated infills align
// each part on itself (grid_center is the origin for a standalone object / feature off).
// Captured before the merge, which grows bb and would otherwise shift its center.
const Point grid_center = bb.center();
bb.merge(align_to_grid(bb.min, Point(period, period), grid_center));
const coord_t xmin = bb.min.x();
const coord_t xmax = bb.max.x();
const coord_t ymin = bb.min.y();
@@ -3146,11 +3155,17 @@ bool FillRectilinear::fill_surface_trapezoidal(
flip_vertical = !flip_vertical;
}
// transpose points for odd infill layers (taking infill combination into account)
// transpose points for odd infill layers (taking infill combination into account).
// Orca: mirror across the diagonal through grid_center (not the origin), so the swapped
// layers stay aligned with the center-phased grid. For a standalone object / feature off,
// grid_center is the origin and this is a plain x/y swap.
if (infill_layer_id % 2 == 1) {
for (Polyline& pl : polylines) {
for (Point& p : pl.points) {
std::swap(p.x(), p.y());
const coord_t dx = p.x() - grid_center.x();
const coord_t dy = p.y() - grid_center.y();
p.x() = grid_center.x() + dy;
p.y() = grid_center.y() + dx;
}
}
}
@@ -3341,6 +3356,14 @@ bool FillRectilinear::fill_surface_trapezoidal(
break;
}
// Orca: cases 1 & 2 build the pattern symmetrically around the origin, so on their own they
// phase to the global origin and every part shares one grid. Shift the pattern onto the box
// center this->bounding_box carries, so separated infills align each part on itself. The center
// is the origin for a standalone object (or when the feature is off), making this a no-op there.
if (Pattern_type != 0)
for (Polyline &pl : polylines)
pl.translate(rotate_vector.second);
// Apply multiline fill
multiline_fill(polylines, params, spacing);
@@ -3553,7 +3576,7 @@ Polylines FillLateralHoneycomb::fill_surface(const Surface *surface, const FillP
// |
// |
// 0 --+--
// / \
// ⟋ ⟍
// why inverted?
// it makes determining some of the properties easier
// and the two angled legs provide additional horizontal stiffness
+3 -3
View File
@@ -61,7 +61,7 @@ static inline FlowRole opt_key_to_flow_role(const std::string &opt_key)
static inline void throw_on_missing_variable(const std::string &opt_key, const char *dependent_opt_key)
{
throw FlowErrorMissingVariable((boost::format(L("Failed to calculate line width of %1%. Cannot get value of \u201c%2%\u201d ")) % opt_key % dependent_opt_key).str());
throw FlowErrorMissingVariable((boost::format("Failed to calculate line width of %1%. Cannot get value of \u201c%2%\u201d.") % opt_key % dependent_opt_key).str());
}
// Used to provide hints to the user on default extrusion width values, and to provide reasonable values to the PlaceholderParser.
@@ -129,7 +129,7 @@ double Flow::extrusion_width(const std::string& opt_key, const ConfigOptionResol
Flow Flow::new_from_config_width(FlowRole role, const ConfigOptionFloatOrPercent &width, float nozzle_diameter, float height)
{
if (height <= 0)
throw Slic3r::InvalidArgument("Invalid flow height supplied to new_from_config_width()");
throw Slic3r::InvalidArgument("Invalid flow height supplied to new_from_config_width().");
float w;
if (!width.percent && width.value <= 0.) {
@@ -157,7 +157,7 @@ Flow Flow::with_spacing(float new_spacing) const
assert(m_width >= m_height);
out.m_width += new_spacing - m_spacing;
if (out.m_width < out.m_height)
throw Slic3r::InvalidArgument(L("Invalid spacing supplied to Flow::with_spacing(), check your layer height and extrusion width"));
throw Slic3r::InvalidArgument("Invalid spacing supplied to Flow::with_spacing(), check your layer height and extrusion width.");
}
out.m_spacing = new_spacing;
return out;
+1 -1
View File
@@ -712,7 +712,7 @@ unsigned int Step::get_triangle_num(double linear_deflection, double angle_defle
return 0;
}
}
} catch(Exception e) {
} catch(const Exception &e) {
return 0;
}
+1 -1
View File
@@ -78,7 +78,7 @@ public:
Standard_Boolean UserBreak() override { return should_stop.load(); }
void Show(const Message_ProgressScope&, const Standard_Boolean) override {
std::cout << "Progress: " << GetPosition() << "%" << std::endl;
std::cout << "Progress: " << std::fixed << std::setprecision(2) << 100.0 * GetPosition() << "%" << std::endl;
}
private:
std::atomic<bool>& should_stop;
+168 -11
View File
@@ -347,6 +347,7 @@ static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_laye
static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode";
static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode";
static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps";
static constexpr const char* FILAMENT_VOL_MAP_ATTR = "filament_volume_maps";
static constexpr const char* LIMIT_FILAMENT_MAP_ATTR = "limit_filament_maps";
static constexpr const char* GCODE_FILE_ATTR = "gcode_file";
static constexpr const char* THUMBNAIL_FILE_ATTR = "thumbnail_file";
@@ -699,6 +700,35 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
info.id = it->first;
info.used_g = used_filament_g;
info.used_m = used_filament_m;
// Stamp each filament's logical-nozzle assignment onto the saved 3mf so the device/monitor can
// reconstruct it. This block runs for every print: reorder_extruders_for_minimum_flush_volume
// runs unconditionally and stores a (non-null) 1-nozzle result even for a single-extruder print,
// so result->nozzle_group_result is non-null here for single-nozzle printers too. The stamped
// nozzle_diameter is the grouping result's rounded matching-key value; the 3mf writer decides the
// final saved diameter (see has_multi_nozzle_extruder). group_id and volume_type are unaffected.
if (result && result->nozzle_group_result) {
auto nozzles_for_filament = result->nozzle_group_result->get_nozzles_for_filament(it->first);
if (!nozzles_for_filament.empty()) {
info.group_id.reserve(nozzles_for_filament.size());
std::set<double> diameters;
std::set<NozzleVolumeType> volume_types;
for (const auto& nozzle : nozzles_for_filament) {
info.group_id.emplace_back(nozzle.group_id);
diameters.insert(string_to_double_decimal_point(nozzle.diameter));
volume_types.insert(nozzle.volume_type);
}
std::sort(info.group_id.begin(), info.group_id.end());
info.group_id.erase(std::unique(info.group_id.begin(), info.group_id.end()), info.group_id.end());
if (!diameters.empty())
info.nozzle_diameter = *diameters.begin();
if (volume_types.size() > 1)
info.nozzle_volume_type = get_nozzle_volume_type_string(nvtHybrid);
else if (!volume_types.empty())
info.nozzle_volume_type = get_nozzle_volume_type_string(*volume_types.begin());
}
}
auto model_volume_it = ps.model_volumes_per_extruder.find(it->first);
auto support_volume_it = ps.support_volumes_per_extruder.find(it->first);
info.used_for_object = model_volume_it != ps.model_volumes_per_extruder.end() && model_volume_it->second > EPSILON;
@@ -706,6 +736,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
slice_filaments_info.push_back(info);
}
// Carry the layer-aware grouping result into the plate so the 3mf writer can emit the <nozzle> tags
// and the enable_filament_dynamic_map flag. Only a LayeredNozzleGroupResult (the slicer output) is
// stored; a device-side StaticNozzleGroupResult loaded from a 3mf is not re-serialized here.
auto layered_group_result = std::dynamic_pointer_cast<MultiNozzleUtils::LayeredNozzleGroupResult>(result->nozzle_group_result);
if (layered_group_result)
nozzle_group_result = *layered_group_result;
/* only for test
GCodeProcessorResult::SliceWarning sw;
sw.msg = BED_TEMP_TOO_HIGH_THAN_FILAMENT;
@@ -1283,6 +1320,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool _handle_start_config_warning(const char** attributes, unsigned int num_attributes);
bool _handle_end_config_warning();
bool _handle_start_config_nozzle(const char** attributes, unsigned int num_attributes);
bool _handle_end_config_nozzle();
//BBS: add plater config parse functions
bool _handle_start_config_plater(const char** attributes, unsigned int num_attributes);
bool _handle_end_config_plater();
@@ -1618,8 +1658,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
plate->is_label_object_enabled = it->second->is_label_object_enabled;
plate->skipped_objects = it->second->skipped_objects;
plate->slice_filaments_info = it->second->slice_filaments_info;
plate->nozzles_info = it->second->nozzles_info;
plate->printer_model_id = it->second->printer_model_id;
plate->nozzle_diameters = it->second->nozzle_diameters;
plate->nozzle_volume_types = it->second->nozzle_volume_types;
plate->filament_maps = it->second->filament_maps;
plate->filament_change_sequence = it->second->filament_change_sequence;
plate->nozzle_change_sequence = it->second->nozzle_change_sequence;
@@ -2289,9 +2331,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
plate_data_list[it->first-1]->is_support_used = it->second->is_support_used;
plate_data_list[it->first-1]->is_label_object_enabled = it->second->is_label_object_enabled;
plate_data_list[it->first-1]->slice_filaments_info = it->second->slice_filaments_info;
plate_data_list[it->first-1]->nozzles_info = it->second->nozzles_info;
plate_data_list[it->first-1]->skipped_objects = it->second->skipped_objects;
plate_data_list[it->first-1]->printer_model_id = it->second->printer_model_id;
plate_data_list[it->first-1]->nozzle_diameters = it->second->nozzle_diameters;
plate_data_list[it->first-1]->nozzle_volume_types = it->second->nozzle_volume_types;
plate_data_list[it->first-1]->filament_maps = it->second->filament_maps;
plate_data_list[it->first-1]->filament_change_sequence = it->second->filament_change_sequence;
plate_data_list[it->first-1]->nozzle_change_sequence = it->second->nozzle_change_sequence;
@@ -3469,6 +3513,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
res = _handle_start_config_filament(attributes, num_attributes);
else if (::strcmp(SLICE_WARNING_TAG, name) == 0)
res = _handle_start_config_warning(attributes, num_attributes);
else if (::strcmp(NOZZLE_TAG, name) == 0)
res = _handle_start_config_nozzle(attributes, num_attributes);
else if (::strcmp(ASSEMBLE_TAG, name) == 0)
res = _handle_start_assemble(attributes, num_attributes);
else if (::strcmp(ASSEMBLE_ITEM_TAG, name) == 0)
@@ -3503,6 +3549,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
res = _handle_end_config_plater();
else if (::strcmp(FILAMENT_TAG, name) == 0)
res = _handle_end_config_filament();
else if (::strcmp(NOZZLE_TAG, name) == 0)
res = _handle_end_config_nozzle();
else if (::strcmp(INSTANCE_TAG, name) == 0)
res = _handle_end_config_plater_instance();
else if (::strcmp(ASSEMBLE_TAG, name) == 0)
@@ -4460,6 +4508,21 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_curr_plater->config.set_key_value("filament_map", new ConfigOptionInts(filament_map));
}
}
else if (key == FILAMENT_VOL_MAP_ATTR) {
if (m_curr_plater){
auto filament_volume_map = get_vector_from_string(value);
for (size_t idx = 0; idx < filament_volume_map.size(); ++idx) {
// The map feeds per-filament slot resolution and grouping. Clamp any
// higher volume-type back to Standard(0) on load: Hybrid(2) is only an
// in-memory grouping seed that is never persisted, and TPU High Flow(3)
// is clamped with the same information loss on every load.
if (filament_volume_map[idx] > 1) {
filament_volume_map[idx] = 0;
}
}
m_curr_plater->config.set_key_value("filament_volume_map", new ConfigOptionInts(filament_volume_map));
}
}
else if (key == GCODE_FILE_ATTR)
{
m_curr_plater->gcode_file = value;
@@ -4569,6 +4632,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (m_curr_plater)
m_curr_plater->printer_model_id = value;
}
else if (key == NOZZLE_VOLUME_TYPE_ATTR)
{
if (m_curr_plater)
m_curr_plater->nozzle_volume_types = value;
}
else if (key == NOZZLE_DIAMETERS_ATTR)
{
if (m_curr_plater)
@@ -4622,6 +4690,41 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return true;
}
bool _BBS_3MF_Importer::_handle_start_config_nozzle(const char** attributes, unsigned int num_attributes)
{
// Read the per-plate <nozzle> tags. Older 3mf without <nozzle> tags leave nozzles_info
// empty; load_nozzle_infos_with_compatibility then rebuilds the list from the per-filament
// group_id / filament_map on the device side.
if (m_curr_plater) {
// id="0" extruder_id="1" nozzle_diameter="0.4" volume_type="Standard"
std::string id = bbs_get_attribute_value_string(attributes, num_attributes, "id");
std::string extruder_id = bbs_get_attribute_value_string(attributes, num_attributes, "extruder_id");
std::string nozzle_diameter= bbs_get_attribute_value_string(attributes, num_attributes, "nozzle_diameter");
std::string volume_type = bbs_get_attribute_value_string(attributes, num_attributes, "volume_type");
auto volume_type_str_to_enum = ConfigOptionEnum<NozzleVolumeType>::get_enum_values();
MultiNozzleUtils::NozzleInfo nozzle_info;
nozzle_info.group_id = atoi(id.c_str());
nozzle_info.extruder_id = atoi(extruder_id.c_str()) - 1;
nozzle_info.diameter = nozzle_diameter;
if (volume_type_str_to_enum.count(volume_type))
nozzle_info.volume_type = NozzleVolumeType(volume_type_str_to_enum.at(volume_type));
else
nozzle_info.volume_type = NozzleVolumeType::nvtStandard;
m_curr_plater->nozzles_info.push_back(nozzle_info);
}
return true;
}
bool _BBS_3MF_Importer::_handle_end_config_nozzle()
{
// do nothing
return true;
}
bool _BBS_3MF_Importer::_handle_start_config_warning(const char** attributes, unsigned int num_attributes)
{
if (m_curr_plater) {
@@ -5949,7 +6052,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_thumbnail_middle = iter->second;
}
boost::system::error_code ec;
std::string filename = std::string(store_params.path);
std::string filename = store_params.path;
boost::filesystem::remove(filename + ".tmp", ec);
bool result = _save_model_to_file(filename + ".tmp", *store_params.model, store_params.plate_data_list, store_params.project_presets, store_params.config,
@@ -7980,6 +8083,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << "\"/>\n";
}
ConfigOptionInts* filament_volume_maps_opt = plate_data->config.option<ConfigOptionInts>("filament_volume_map");
if (filament_map_mode_opt != nullptr && filament_volume_maps_opt != nullptr) {
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << FILAMENT_VOL_MAP_ATTR << "\" " << VALUE_ATTR << "=\"";
const std::vector<int>& volume_values = filament_volume_maps_opt->values;
for (int i = 0; i < volume_values.size(); ++i) {
stream << volume_values[i];
if (i != (volume_values.size() - 1))
stream << " ";
}
stream << "\"/>\n";
}
if (save_gcode)
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << GCODE_FILE_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << xml_escape(plate_data->gcode_file) << "\"/>\n";
if (!plate_data->gcode_file.empty()) {
@@ -8119,7 +8234,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
[](unsigned int filament_id) { return filament_id + 1; });
const std::string plate_key = "plate_" + std::to_string(idx + 1);
sequence_json[plate_key]["sequence"] = filament_sequence;
// Dynamic-map plates write the sequence under "filament_sequence"; every other plate (the
// whole shipping fleet + H2C static mode) keeps the "sequence" key, so the saved 3mf is
// byte-identical to the older format. The reader accepts both.
const bool enable_dynamic_map = plate_data->nozzle_group_result && plate_data->nozzle_group_result->is_support_dynamic_nozzle_map();
const std::string seq_key = enable_dynamic_map ? "filament_sequence" : "sequence";
sequence_json[plate_key][seq_key] = filament_sequence;
sequence_json[plate_key]["nozzle_sequence"] = plate_data->nozzle_change_sequence;
sequence_json[plate_key]["optimal_assignment"] = plate_data->optimal_assignment;
}
@@ -8205,6 +8325,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (nozzle_diameter_option)
nozzle_diameters_str = nozzle_diameter_option->serialize();
// True when any extruder carries a cluster of interchangeable nozzles (max nozzle count
// > 1). Such an extruder's per-nozzle diameters are not expressible in the per-extruder
// nozzle_diameter config, so the saved <filament>/<nozzle> diameters must come from the
// grouping result. For a single-nozzle-per-extruder printer the true diameter is the raw
// config value; the grouping result rounds it to the nearest of {0.2,0.4,0.6,0.8} for its
// internal matching key, so reading that back would rewrite a non-standard nozzle
// (e.g. 0.5 -> 0.4). Use this flag to keep the exact config value in that case.
auto* extruder_max_nozzle_count_option = dynamic_cast<const ConfigOptionInts*>(config.option("extruder_max_nozzle_count"));
const bool has_multi_nozzle_extruder = extruder_max_nozzle_count_option &&
std::any_of(extruder_max_nozzle_count_option->values.begin(), extruder_max_nozzle_count_option->values.end(),
[](int v) { return v > 1; });
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << PRINTER_MODEL_ID_ATTR << "\" " << VALUE_ATTR << "=\"" << plate_data->printer_model_id << "\"/>\n";
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << NOZZLE_DIAMETERS_ATTR << "\" " << VALUE_ATTR << "=\"" << nozzle_diameters_str << "\"/>\n";
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << TIMELAPSE_TYPE_ATTR << "\" " << VALUE_ATTR << "=\"" << timelapse_type << "\"/>\n";
@@ -8214,7 +8346,15 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << OUTSIDE_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->toolpath_outside << "\"/>\n";
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SUPPORT_USED_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->is_support_used << "\"/>\n";
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << LABEL_OBJECT_ENABLED_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->is_label_object_enabled << "\"/>\n";
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << ENABLE_FILAMENT_DYNAMIC_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << false << "\"/>\n";
// Report the plate's dynamic-map state from the grouping result. The result is present
// for the whole fleet (a static single-nozzle result too), so this if-branch is normally
// taken; is_support_dynamic_nozzle_map() is false for any non-dynamic (static /
// single-extruder) result ⇒ byte-identical to the previously hard-coded value. The else
// is a defensive fallback for a missing result.
if (plate_data && plate_data->nozzle_group_result)
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << ENABLE_FILAMENT_DYNAMIC_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << plate_data->nozzle_group_result->is_support_dynamic_nozzle_map() << "\"/>\n";
else
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << ENABLE_FILAMENT_DYNAMIC_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << false << "\"/>\n";
{
bool has_filament_switcher = config.has("has_filament_switcher") ? config.opt_bool("has_filament_switcher") : false;
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << HAS_FILAMENT_SWITCHER_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << has_filament_switcher << "\"/>\n";
@@ -8329,7 +8469,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (std::find(used_nozzle_groups.begin(), used_nozzle_groups.end(), nozzle_group_id) == used_nozzle_groups.end())
used_nozzle_groups.push_back(nozzle_group_id);
const std::string filament_nozzle_group_id = it->group_id.empty() ? std::to_string(nozzle_group_id) : join_int_list_comma(it->group_id);
const double filament_nozzle_diameter = it->nozzle_diameter > 0.0 ? it->nozzle_diameter : get_nozzle_diameter(nozzle_group_id);
// Single-nozzle extruders: exact config diameter; clusters keep the result's rounded
// value (see has_multi_nozzle_extruder).
const double filament_nozzle_diameter = (has_multi_nozzle_extruder && it->nozzle_diameter > 0.0)
? it->nozzle_diameter : get_nozzle_diameter(nozzle_group_id);
const std::string filament_nozzle_volume_type = it->nozzle_volume_type.empty() ? get_nozzle_volume_type(nozzle_group_id) : it->nozzle_volume_type;
stream << " <" << FILAMENT_TAG << " " << FILAMENT_ID_TAG << "=\"" << std::to_string(it->id + 1) << "\" "
@@ -8349,12 +8492,26 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << " <" << SLICE_WARNING_TAG << " msg=\"" << it->msg << "\" level=\"" << std::to_string(it->level) << "\" error_code =\"" << it->error_code << "\" />\n";
}
for (int nozzle_group_id : used_nozzle_groups) {
stream << " <" << NOZZLE_TAG << " "
<< "id=\"" << nozzle_group_id << "\" "
<< "extruder_id=\"" << nozzle_group_id + 1 << "\" "
<< "nozzle_diameter=\"" << get_nozzle_diameter_str(nozzle_group_id) << "\" "
<< "volume_type=\"" << get_nozzle_volume_type(nozzle_group_id) << "\"/>\n";
// Emit the <nozzle> tags from the grouping result. Single-nozzle-per-extruder printers
// override the diameter with the exact config value (see has_multi_nozzle_extruder); the
// else is a defensive fallback for a missing result.
if (plate_data->nozzle_group_result) {
auto used_nozzle_list = plate_data->nozzle_group_result->get_used_nozzles_in_extruder();
for (auto& used_nozzle : used_nozzle_list) {
if (!has_multi_nozzle_extruder && nozzle_diameter_option &&
used_nozzle.extruder_id >= 0 && used_nozzle.extruder_id < (int) nozzle_diameter_option->values.size()) {
used_nozzle.diameter = get_nozzle_diameter_str(used_nozzle.extruder_id);
}
stream << " <" << NOZZLE_TAG << " " << used_nozzle.serialize() << "/>\n";
}
} else {
for (int nozzle_group_id : used_nozzle_groups) {
stream << " <" << NOZZLE_TAG << " "
<< "id=\"" << nozzle_group_id << "\" "
<< "extruder_id=\"" << nozzle_group_id + 1 << "\" "
<< "nozzle_diameter=\"" << get_nozzle_diameter_str(nozzle_group_id) << "\" "
<< "volume_type=\"" << get_nozzle_volume_type(nozzle_group_id) << "\"/>\n";
}
}
if (!plate_data->layer_filaments.empty()) {
@@ -8988,7 +9145,7 @@ bool store_bbs_3mf(StoreParams& store_params)
// All export should use "C" locales for number formatting.
CNumericLocalesSetter locales_setter;
if (store_params.path == nullptr || store_params.model == nullptr)
if (store_params.path.empty() || store_params.model == nullptr)
return false;
_BBS_3MF_Exporter exporter;
+9 -1
View File
@@ -73,6 +73,7 @@ struct PlateData
std::map<int, std::pair<int, int>> obj_inst_map;
std::string printer_model_id;
std::string nozzle_diameters;
std::string nozzle_volume_types;
std::string gcode_file;
std::string gcode_file_md5;
std::string thumbnail_file;
@@ -102,6 +103,13 @@ struct PlateData
std::vector<unsigned int> nozzle_change_sequence;
std::vector<int> optimal_assignment;
// Multi-nozzle grouping surface. nozzles_info accumulates the <nozzle> tags read from a
// gcode.3mf; nozzle_group_result is the slicer's per-filament→nozzle assignment carried into the
// saved 3mf metadata (write) and reconstructed on load. Both are empty/nullopt for single-nozzle
// prints, so the saved-3mf output for single-nozzle printers is byte-identical.
std::vector<MultiNozzleUtils::NozzleInfo> nozzles_info;
std::optional<MultiNozzleUtils::LayeredNozzleGroupResult> nozzle_group_result;
// Hexadecimal number,
// the 0th digit corresponds to extruder 1
// the 1th digit corresponds to extruder 2
@@ -226,7 +234,7 @@ typedef std::map<int, PlateData*> PlateDataMaps;
struct StoreParams
{
const char* path;
std::string path;
Model* model = nullptr;
PlateDataPtrs plate_data_list;
int export_plate_idx = -1;
+1715 -415
View File
File diff suppressed because it is too large Load Diff
+111 -8
View File
@@ -60,15 +60,20 @@ class Wipe {
public:
bool enable;
Polyline path;
// Orca:
struct RetractionValues{
double retractLengthBeforeWipe;
double retractLengthDuringWipe;
double retraction_length_before_wipe = 0.;
double retraction_length_during_wipe = 0.;
double retraction_length_after_wipe = 0.;
};
Wipe() : enable(false) {}
bool has_path() const { return !this->path.points.empty(); }
void reset_path() { this->path = Polyline(); }
std::string wipe(GCode &gcodegen, double length, bool toolchange = false, bool is_last = false);
// Orca:
RetractionValues calculateWipeRetractionLengths(GCode& gcodegen, bool toolchange);
};
@@ -125,8 +130,11 @@ public:
private:
WipeTowerIntegration& operator=(const WipeTowerIntegration&);
std::string append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const BoundingBox &printer_bbx) const;
Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const Polygons &bed_polygons) const;
std::string append_tcr2(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
std::string travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const;
Vec2f transform_wt2_pt(const Vec2f &pt) const;
Polygons shared_printable_area(GCode &gcodegen) const;
// Postprocesses gcode: rotates and moves G1 extrusions and returns result
std::string post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const;
@@ -176,7 +184,7 @@ struct LayerResult {
// It is used for the pressure equalizer because it needs to buffer one layer back.
bool nop_layer_result { false };
static LayerResult make_nop_layer_result() { return {"", std::numeric_limits<coord_t>::max(), false, false, true}; }
static LayerResult make_nop_layer_result() { return {"", std::numeric_limits<size_t>::max(), false, false, true}; }
};
class GCode {
@@ -252,8 +260,9 @@ public:
std::string travel_to(const Point& point, ExtrusionRole role, std::string comment, double z = DBL_MAX);
bool needs_retraction(const Polyline& travel, ExtrusionRole role, LiftType& lift_type);
std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone);
std::string unretract() { return m_writer.unlift() + m_writer.unretract(); }
std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1);
// extra_retract forwards a PETG pre-extrusion over-extrusion; default 0 -> identical to the plain deretract.
std::string unretract(float extra_retract = 0.f) { return m_writer.unlift() + m_writer.unretract(extra_retract); }
std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1, bool defer_temp_wait = false);
bool is_BBL_Printer();
WipeTowerType wipe_tower_type();
@@ -263,6 +272,12 @@ public:
// append full config to the given string
static void append_full_config(const Print& print, std::string& str);
// Per-filament config-slot resolvers for the current layer (m_cur_layer_idx): the filament
// resolver keys filament-indexed arrays, the nozzle resolver keys (extruder x volume-type)
// slot arrays. Both degenerate to filament_id / extruder index on single-volume printers.
size_t get_filament_config_index(int filament_id) const;
size_t get_nozzle_config_index(int filament_id) const;
// Object and support extrusions of the same PrintObject at the same print_z.
// public, so that it could be accessed by free helper functions from GCode.cpp
struct LayerToPrint
@@ -347,11 +362,13 @@ private:
std::vector<coordf_t> &skirt_done);
std::string generate_object_skirt_group(const Print &print,
const PrintObject &object,
size_t instance_id,
const LayerTools &layer_tools,
const Layer& layer,
unsigned int extruder_id);
std::string generate_object_brim(const Print &print,
const PrintObject &object,
size_t instance_id,
bool first_layer);
LayerResult process_layer(
@@ -394,6 +411,7 @@ private:
void check_placeholder_parser_failed();
size_t cur_extruder_index() const;
size_t get_extruder_id(unsigned int filament_id) const;
void update_placeholder_parser_with_variant_params();
void set_last_pos(const Point &pos) { m_last_pos = Point3(pos, 0); m_last_pos_defined = true; }
void set_last_pos(const Point3 &pos) { m_last_pos = pos; m_last_pos_defined = true; }
@@ -402,6 +420,11 @@ private:
std::string preamble();
// BBS
std::string change_layer(coordf_t print_z);
// Bedslinger model: derive the Y-axis acceleration limit from the machine force/bed-mass config
// and the mass already printed. Yields the min machine Y acceleration when the A2L config keys are
// unset (i.e. every existing printer), so it is inert for them.
void mass_load_limited_machine_acceleration(const PrintStatistics &curr_print_statistics, const Print &print,
double &y_acceleration_limit_res, double &accumulated_mass_res);
// Orca: pass the complete collection of region perimeters to the extrude loop to check whether the wipe before external loop
// should be executed
std::string extrude_entity(const ExtrusionEntity& entity,
@@ -500,14 +523,59 @@ private:
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);
// Farthest-point timelapse: find the extrusion point farthest from camera (0,0)
void compute_farthest_point(const std::vector<LayerToPrint> &layers, int most_used_extruder,
const std::map<std::pair<const SupportLayer *, ExtrusionRole>, unsigned int> &support_filaments);
// Build the per-layer timelapse snapshot g-code (safe-position or, when skip_pos_pick,
// an inline photo at the current head position). Extracted from the former process_layer lambda so the
// per-extrusion farthest-point hook (_extrude) can call it too. Identical to the old lambda when the
// farthest-point subsystem is disabled (skip_pos_pick=false, m_farthest_point_timelapse.enabled=false).
std::string generate_timelapse_gcode(const Print &print, coordf_t print_z, int most_used_extruder,
const std::set<size_t> *layer_object_label_ids,
const std::vector<const PrintObject*> *printed_objects,
bool skip_pos_pick = false);
// BBS
LiftType to_lift_type(ZHopType z_hop_types);
std::set<ObjectID> m_objsWithBrim; // indicates the objs with brim
std::set<ObjectID> m_objSupportsWithBrim; // indicates the objs' supports with brim
std::set<ObjectInstanceID> m_objsWithBrim; // indicates the object instances with brim
// Cache for custom seam enforcers/blockers for each layer.
SeamPlacer m_seam_placer;
// One stop of the island-level tour: consecutive islands of a single instance. An instance
// can have several visits per layer when its islands are toured non-consecutively.
struct InstanceVisit
{
// Index into the per-filament InstanceToPrint vector.
size_t instance_idx;
// Islands to print, in order (indices into ObjectByExtruder::islands). Empty: print all
// islands, ordered at extrusion time.
std::vector<size_t> islands;
// First visit of this instance this layer; skirt, brim and support are emitted here.
bool first_visit;
};
// One node of the island-level tour, also used as cache key: identity plus quantized position.
struct IslandOrderNode
{
ObjectID object_id;
size_t instance_id;
// Index into ObjectByExtruder::islands, or size_t(-1) for an instance without chainable
// islands (e.g. support only), which is toured as a single stop.
size_t island_idx;
// Island centroid in G-code coordinates, quantized to 1 mm for cache stability.
Point pos;
bool operator==(const IslandOrderNode &rhs) const {
return object_id == rhs.object_id && instance_id == rhs.instance_id &&
island_idx == rhs.island_idx && pos == rhs.pos;
}
};
// Cache the per-filament island tour to avoid recomputing while the layer's island layout is
// unchanged. Key: filament_id. Value: {nodes the tour was computed from, resulting visits}.
std::map<unsigned int, std::pair<std::vector<IslandOrderNode>, std::vector<InstanceVisit>>>
m_ordering_cache;
ExtrusionQualityEstimator m_extrusion_quality_estimator;
@@ -556,6 +624,32 @@ private:
AvoidCrossingPerimeters m_avoid_crossing_perimeters;
RetractWhenCrossingPerimeters m_retract_when_crossing_perimeters;
TimelapsePosPicker m_timelapse_pos_picker;
// Farthest-point timelapse context. Corexy-only refinement layered on top of the existing
// timelapse_type. All fields default to the inert state; `enabled` is (re)computed each layer in
// process_layer and is false whenever the farthest_point_timelapse config toggle is off, the printer
// is i3 (psI3), or timelapse_type is not traditional — so every shipping printer that does not set the
// toggle is identical to the previous path.
struct FarthestPointTimelapseContext {
// Whether farthest-point timelapse is active for this layer
bool enabled{false};
// The farthest extrusion point from camera (0,0) in global scaled coordinates (includes plate origin + inst.shift)
Point farthest_point;
// farthest_point converted to mm (gcode coordinate space, includes plate origin)
Vec2d farthest_gcode_pos{0, 0};
// Extruder index (0-based) that prints the farthest point
int farthest_extruder_id{0};
// Whether the farthest point is printed by the photo head (most_used_extruder)
bool farthest_is_photo_head{false};
// Whether inline timelapse gcode has already been inserted on this layer
bool inserted_this_layer{false};
// The extruder used most on this layer, chosen as the photo head
int most_used_extruder{0};
// Object labels for the current layer, used when inline timelapse is inserted from extrusion code.
std::set<size_t> layer_object_label_ids;
};
FarthestPointTimelapseContext m_farthest_point_timelapse;
bool m_enable_loop_clipping;
//resonance avoidance
bool m_resonance_avoidance;
@@ -595,6 +689,9 @@ private:
float m_last_layer_z{ 0.0f };
float m_max_layer_z{ 0.0f };
float m_last_width{ 0.0f };
// Bedslinger mass model: cumulative printed mass at the previous layer, used to derive
// the current layer mass for the per-layer Y acceleration limit (curr_y_acceleration_limit).
double m_last_layer_accumulated_mass{ 0.0 };
// Always check gcode placeholders when building in debug mode.
#if !defined(NDEBUG)
@@ -654,12 +751,18 @@ private:
int m_start_gcode_filament = -1;
std::string m_filament_instances_code;
// Object layer id of the layer being generated; keys the per-filament config-slot
// resolvers. Distinct from m_layer_index (an export progress counter starting at -1).
size_t m_cur_layer_idx{0};
std::set<unsigned int> m_initial_layer_extruders;
std::vector<std::vector<unsigned int>> m_sorted_layer_filaments;
// BBS
int get_bed_temperature(const int extruder_id, const bool is_first_layer, const BedType bed_type) const;
int get_highest_bed_temperature(const bool is_first_layer,const Print &print) const;
void update_layer_related_config(int layer_id);
double calc_max_volumetric_speed(const double layer_height, const double line_width, const std::string co_str);
std::string _extrude(const ExtrusionPath &path, std::string description = "", double speed = -1);
bool _needSAFC(const ExtrusionPath &path);
+7 -2
View File
@@ -844,7 +844,10 @@ std::string CoolingBuffer::apply_layer_cooldown(
ironing_fan_control = false; // ORCA: Add support for ironing fan speed control
ironing_fan_speed = 0; // ORCA: Add support for ironing fan speed control
}
if (fan_speed_new != m_fan_speed) {
// A tool change may keep the same configured base fan speed while the physical fan is
// still running at the previous filament's overhang speed. Restore the base speed before
// emitting G-code for the new tool in that case.
if (fan_speed_new != m_fan_speed || (immediately_apply && m_current_fan_speed != fan_speed_new)) {
m_fan_speed = fan_speed_new;
m_current_fan_speed = fan_speed_new;
if (immediately_apply)
@@ -1040,8 +1043,10 @@ std::string CoolingBuffer::apply_layer_cooldown(
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_current_fan_speed, part_cooling_fan_min_pwm);
fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false;
}
else
else {
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed, part_cooling_fan_min_pwm);
m_current_fan_speed = m_fan_speed;
}
need_set_fan = false;
}
pos = line_end;
@@ -0,0 +1,190 @@
#include "GCodeProcessor.hpp"
#include "libslic3r/libslic3r.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <string_view>
namespace Slic3r {
namespace {
bool equals_case_insensitive(std::string_view lhs, std::string_view rhs)
{
return lhs.size() == rhs.size() && std::equal(lhs.begin(), lhs.end(), rhs.begin(), [](unsigned char l, unsigned char r) {
return std::tolower(l) == std::tolower(r);
});
}
float get_clamped_param(const GCodeReader::GCodeLine& line, char axis, float default_value, float min_value, float max_value)
{
float value = default_value;
line.has_value(axis, value);
return std::clamp(value, min_value, max_value);
}
float extrusion_time(float e_length, float feedrate)
{
return feedrate > 0.0f && e_length > 0.0f ? e_length / feedrate * 60.0f : 0.0f;
}
float retract_time(float e_length)
{
static constexpr float retract_feedrate = 1800.0f;
return extrusion_time(std::max(e_length, 0.0f), retract_feedrate);
}
float s819_time(float e_length, float feedrate)
{
static constexpr float s819_tail_flush_length = 10.0f;
static constexpr float s819_tail_feedrate = 400.0f;
const float tail_length = std::min(std::max(e_length, 0.0f), s819_tail_flush_length);
const float main_length = std::max(e_length - tail_length, 0.0f);
return extrusion_time(main_length, feedrate) + extrusion_time(tail_length, s819_tail_feedrate);
}
float estimate_M6211_time_for_centauri_carbon(const GCodeReader::GCodeLine& line, float length, double current_x,
double current_y)
{
static constexpr float max_segment_length = 73.0f;
static constexpr float wipe_after_flush_time = 2.8f;
static constexpr float main_feedrate = 500.0f;
static constexpr float tail_feedrate = 400.0f;
static constexpr float travel_feedrate = 5000.0f;
static constexpr double parking_x = 256.0;
static constexpr double parking_y = 0.0;
const float flush_length = std::clamp(length, 10.0f, 1000.0f);
const float cool_time = get_clamped_param(line, 'P', 5000.0f, 0.0f, 20000.0f) * 0.001f;
const float travel_time = static_cast<float>(std::abs(current_y - parking_y) + std::abs(current_x - parking_x)) /
travel_feedrate * 60.0f;
// Initial time, including: material change, heating, etc.
float m6211_time = 18.2f + travel_time;
float remaining_flush_length = std::max(flush_length, 0.0f);
while (remaining_flush_length > 0.0f) {
const float segment_length = std::min(remaining_flush_length, max_segment_length);
remaining_flush_length -= segment_length;
if (segment_length >= max_segment_length) {
// Full segment: 3-phase extrusion (30+35+10=75mm) + retract
m6211_time += extrusion_time(30.0f, main_feedrate) + extrusion_time(35.0f, main_feedrate) +
extrusion_time(10.0f, tail_feedrate) + extrusion_time(2.0f, tail_feedrate) + cool_time +
wipe_after_flush_time;
} else {
// Partial last segment: simple extrude at F500 + retract at F400
m6211_time += extrusion_time(segment_length, main_feedrate) + extrusion_time(2.0f, tail_feedrate) + cool_time +
wipe_after_flush_time;
}
}
return m6211_time;
}
float estimate_M6211_time_for_centauri_carbon_2(const GCodeReader::GCodeLine& line, float length, float new_extruder_temp)
{
const float flush_length = std::clamp(length, 10.0f, 1000.0f);
const float flush_length_single = get_clamped_param(line, 'K', 75.0f, 10.0f, 300.0f);
const float old_filament_e_feedrate = get_clamped_param(line, 'M', 300.0f, 10.0f, 600.0f);
const float new_filament_e_feedrate = get_clamped_param(line, 'N', 300.0f, 10.0f, 600.0f);
const float cool_time = get_clamped_param(line, 'P', 3000.0f, 0.0f, 20000.0f) * 0.001f;
// The flush length of the old material, unit: mm
static constexpr float e_flush_dist = 15.0f;
// Wipe time after flush, in seconds
static constexpr float wipe_after_flush_time = 5.0f;
const float flush_length_after_start = std::max(flush_length - e_flush_dist, 0.0f);
const int flush_times = std::max(1, static_cast<int>(std::ceil(flush_length_after_start / flush_length_single)));
const float flush_length_actual = flush_length_single;
// Initial time, including: material change, heating, moving, etc.
float m6211_time = 31.0f;
m6211_time += extrusion_time(std::min(e_flush_dist, flush_length), old_filament_e_feedrate);
const int intermediate_flush_times = flush_times - 1;
const float intermediate_flush_time = s819_time(flush_length_actual, new_filament_e_feedrate) + retract_time(6.0f) + cool_time +
wipe_after_flush_time;
m6211_time += static_cast<float>(intermediate_flush_times) * intermediate_flush_time;
m6211_time += s819_time(flush_length_actual, new_filament_e_feedrate * 0.8f) + retract_time(4.0f) + cool_time + wipe_after_flush_time;
static constexpr float cooling_rate = 1.36f;
const float r_temp = get_clamped_param(line, 'R', new_extruder_temp + 20.0f, 185.0f, 350.0f);
const float s_temp = get_clamped_param(line, 'S', 250.0f, 185.0f, 350.0f);
if (s_temp < r_temp)
m6211_time += (r_temp - s_temp) / cooling_rate;
return m6211_time;
}
float estimate_M6211_time(const GCodeReader::GCodeLine& line, std::string_view printer_model, float length, float new_extruder_temp, double current_x, double current_y)
{
if (equals_case_insensitive(printer_model, "Elegoo Centauri Carbon") || equals_case_insensitive(printer_model, "Elegoo Centauri")) {
return estimate_M6211_time_for_centauri_carbon(line, length, current_x, current_y);
} else if (equals_case_insensitive(printer_model, "Elegoo Centauri Carbon 2") ||
equals_case_insensitive(printer_model, "Elegoo Centauri 2")) {
return estimate_M6211_time_for_centauri_carbon_2(line, length, new_extruder_temp);
}
return 0.0f;
}
} // namespace
void GCodeProcessor::process_elegoo_M6211(const GCodeReader::GCodeLine& line)
{
float length = 0.0f;
if (!line.has_value('L', length) || length <= 0.0f)
return;
float t = -1.0f;
if (!line.has_value('T', t) || t < 0.0f)
return;
const int filament_id = static_cast<int>(std::round(t));
if (filament_id < 0 || filament_id >= m_result.filaments_count)
return;
const int extruder_id = m_filament_maps[filament_id];
float new_extruder_temp = 0.0f;
if (line.has_value('S', new_extruder_temp)) {
if (extruder_id >= 0 && static_cast<size_t>(extruder_id) < m_extruder_temps.size())
m_extruder_temps[static_cast<size_t>(extruder_id)] = new_extruder_temp;
}
const float m6211_time = estimate_M6211_time(line, m_printer_model, length, new_extruder_temp,
m_start_position[X], m_start_position[Y]);
const int curr_filament_id = get_filament_id(false);
const bool is_first_extrusion = (curr_filament_id == -1) || (filament_id == curr_filament_id);
m_time_processor.filament_unload_times = 0;
m_time_processor.filament_load_times = m6211_time;
process_filament_change(filament_id);
if (extruder_id >= 0 && static_cast<size_t>(extruder_id) < m_remaining_volume.size()) {
const float remaining_volume = static_cast<size_t>(extruder_id) < m_nozzle_volume.size() ?
m_nozzle_volume[extruder_id] :
0.0f;
const float filament_diameter = static_cast<size_t>(filament_id) < m_result.filament_diameters.size() ?
m_result.filament_diameters[filament_id] :
m_result.filament_diameters.back();
const float area_filament_cross_section = static_cast<float>(M_PI) * sqr(0.5f * filament_diameter);
const float volume_flushed_filament = area_filament_cross_section * length;
if (volume_flushed_filament >= remaining_volume) {
if (!is_first_extrusion)
m_used_filaments.update_flush_per_filament(curr_filament_id, remaining_volume);
m_used_filaments.update_flush_per_filament(filament_id, volume_flushed_filament - remaining_volume);
m_remaining_volume[extruder_id] = 0.0f;
} else {
m_used_filaments.update_flush_per_filament(filament_id, volume_flushed_filament);
m_remaining_volume[extruder_id] -= volume_flushed_filament;
}
}
}
} // namespace Slic3r
File diff suppressed because it is too large Load Diff
+376 -3
View File
@@ -6,6 +6,7 @@
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/CustomGCode.hpp"
#include "libslic3r/MultiNozzleUtils.hpp"
#include <cstdint>
#include <array>
@@ -43,6 +44,23 @@ class Print;
Count
};
// Classifies why a wipe-tower / change_filament / time-lapse region is safe to relocate a
// pre-heat M104 into, for the pre-heat/pre-cool injector. The shipping time_lapse_gcode
// template (timelapse-on by default) emits SKIPPABLE_* on essentially every slice, so the
// "timelapse" payload -> stTimelapse classification is exercised widely.
enum SkipType
{
stTimelapse,
stHeadWrapDetect,
stOther,
stNone
};
const std::unordered_map<std::string_view, SkipType> skip_type_map{
{"timelapse", SkipType::stTimelapse},
{"head_wrap_detect", SkipType::stHeadWrapDetect}
};
struct PrintEstimatedStatistics
{
enum class ETimeMode : unsigned char
@@ -77,6 +95,10 @@ class Print;
std::array<Mode, static_cast<size_t>(ETimeMode::Count)> modes;
unsigned int total_filament_changes;
// Number of filament changes that actually re-flush a nozzle (a filament-in-nozzle change
// onto a non-empty nozzle), tracked only by the richer multi-nozzle hotend-change time model.
// Stays 0 for single-nozzle printers (X1/P1/A1/H2S/A2L), which never enter the two-arg model.
unsigned int total_flush_filament_changes;
unsigned int total_extruder_changes;
float total_filament_load_time;
float total_filament_unload_time;
@@ -101,6 +123,7 @@ class Print;
flush_per_filament.clear();
used_filaments_per_role.clear();
total_filament_changes = 0;
total_flush_filament_changes = 0;
total_extruder_changes = 0;
total_filament_load_time = 0.0f;
total_filament_unload_time = 0.0f;
@@ -166,6 +189,14 @@ class Print;
ConflictResultOpt conflict_result;
GCodeCheckResult gcode_check_result;
FilamentPrintableResult filament_printable_reuslt;
// The per-filament -> logical-nozzle grouping the slicer computed for this
// result, surfaced onto the object the device GUI reads
// (plater->background_process().get_current_gcode_result()). Populated only from
// Print::get_layered_nozzle_group_result() (ToolOrdering's static L/R + rack subset);
// default-empty (null) and read by no g-code emitter, so it is invisible in the emitted
// g-code. Consumed by the print-dispatch nozzle mapping (DevNozzleMappingCtrl) via
// DevUtilBackend::GetNozzleGroupResult.
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> nozzle_group_result;
float initial_layer_time;
struct SettingsIds
@@ -263,6 +294,14 @@ class Print;
std::vector<SliceWarning> warnings;
int nozzle_hrc;
std::vector<NozzleType> nozzle_type;
// Per-extruder physical hotend type. Fed to the pre-heat injector's TimeProcessContext
// (mixed-type X2D workaround). Populated in apply_config; unused until the injector side-pass
// consumes it.
std::vector<ExtruderType> extruder_types;
// Machine-slot layout of the per-variant printer arrays (one entry per (extruder x
// volume-type) slot). Populated in apply_config; keys the per-slot machine-limit lookup.
std::vector<std::string> printer_extruder_variant;
std::vector<int> printer_extruder_id;
// first key stores filaments, second keys stores the layer ranges(enclosed) that use the filaments
std::unordered_map<std::vector<unsigned int>, std::vector<std::pair<int, int>>,FilamentSequenceHash> layer_filaments;
std::vector<unsigned int> nozzle_change_sequence;
@@ -271,6 +310,11 @@ class Print;
// first key stores `from` filament, second keys stores the `to` filament
std::map<std::pair<int,int>, int > filament_change_count_map;
// Accumulated print time spent inside SKIPPABLE regions, per skip type. Populated by the time
// estimator; consumed only downstream. The shipping time_lapse_gcode template emits SKIPPABLE_*
// widely, so this is typically populated (stTimelapse) on most slices.
std::unordered_map<SkipType, float> skippable_part_time;
BedType bed_type = BedType::btCount;
void reset();
@@ -304,11 +348,20 @@ class Print;
gcode_check_result = other.gcode_check_result;
limit_filament_maps = other.limit_filament_maps;
filament_printable_reuslt = other.filament_printable_reuslt;
// Orca: copy the shared grouping result so a copied result keeps it (shared_ptr =>
// memory-safe), rather than leaving a stale pointer on the target. No g-code effect either way.
nozzle_group_result = other.nozzle_group_result;
// Keep the per-extruder hotend types on a copied result (injector input).
extruder_types = other.extruder_types;
printer_extruder_variant = other.printer_extruder_variant;
printer_extruder_id = other.printer_extruder_id;
layer_filaments = other.layer_filaments;
filament_change_sequence = other.filament_change_sequence;
nozzle_change_sequence = other.nozzle_change_sequence;
optimal_assignment = other.optimal_assignment;
filament_change_count_map = other.filament_change_count_map;
// Keep the SKIPPABLE per-type time on a copied result.
skippable_part_time = other.skippable_part_time;
initial_layer_time = other.initial_layer_time;
#if ENABLE_GCODE_VIEWER_STATISTICS
time = other.time;
@@ -319,6 +372,75 @@ class Print;
void unlock() const { result_mutex.unlock(); }
};
// First-pass usage-block descriptors for the pre-heat/pre-cool injector. FilamentUsageBlock
// records the [lower,upper) output-line-id span a single filament occupies; ExtruderUsageBlcok
// (the "Blcok" typo is intentional) records the span an extruder is active in, with the start/end
// filament + logical-nozzle ids and the post-extrusion (pre-switch) partial-free sub-range. Built
// during run_post_process, consumed only by the injector side-pass under the enable_pre_heating gate.
namespace ExtruderPreHeating
{
struct FilamentUsageBlock
{
int filament_id;
int extruder_id;
int nozzle_id;
unsigned int lower_gcode_id;
unsigned int upper_gcode_id; // [lower_gcode_id,upper_gcode_id) uses current filament , upper gcode id will be set after finding next block
FilamentUsageBlock(int filament_id_, int extruder_id_, int nozzle_id_, unsigned int lower_gcode_id_, unsigned int upper_gcode_id_) :filament_id(filament_id_), extruder_id(extruder_id_), nozzle_id(nozzle_id_), lower_gcode_id(lower_gcode_id_), upper_gcode_id(upper_gcode_id_) {}
};
/**
* @brief Describle the usage of a exturder in a section
*
* The strucutre stores the start and end lines of the sections as well as
* the filament used at the beginning and end of the section.
* Post extrusion means the final extrusion before switching to the next extruder.
*
* Simplified GCode Flow:
* 1.Extruder Change Block (ext0 switch to ext1)
* 2.Extruder Usage Block (use ext1 to print)
* 3.Extruder Change Block (ext1 switch to ext0)
* 4.Extruder Usage Block (use ext0 to print)
* 5.Extruder Change Block (ext0 switch to ex1)
* ...
*
* So the construct of extruder usage block relys on two extruder change block
*/
struct ExtruderUsageBlcok
{
int extruder_id = -1;
unsigned int start_id = -1;
unsigned int end_id = -1;
int start_filament = -1;
int end_filament = -1;
int start_nozzle_id = -1;
int end_nozzle_id = -1;
unsigned int post_extrusion_start_id = -1;
unsigned int post_extrusion_end_id = -1;
bool ignore_cooling_before_tower = false;
void initialize_step_1(int extruder_id_, int start_id_, int start_filament_, int start_nozzle_id_) {
extruder_id = extruder_id_;
start_id = start_id_;
start_filament = start_filament_;
start_nozzle_id = start_nozzle_id_;
};
void initialize_step_2(int post_extrusion_start_id_) {
post_extrusion_start_id = post_extrusion_start_id_;
}
void initialize_step_3(int end_id_, int end_filament_, int post_extrusion_end_id_, int end_nozzle_id_) {
end_id = end_id_;
end_filament = end_filament_;
post_extrusion_end_id = post_extrusion_end_id_;
end_nozzle_id = end_nozzle_id_;
}
void reset() {
*this = ExtruderUsageBlcok();
}
ExtruderUsageBlcok() = default;
};
}
class CommandProcessor {
public:
@@ -347,6 +469,24 @@ class Print;
static const std::string VFlush_Start_Tag;
static const std::string VFlush_End_Tag;
static const std::string External_Purge_Tag;
public:
// Orca: SKIPPABLE region tags, stored as static strings (the FLUSH idiom above) rather than
// a CustomETags/CustomTags array. Public so the emission sites (WipeTower / change_filament
// path) can reference them single-sourced.
static const std::string Skippable_Start_Tag;
static const std::string Skippable_End_Tag;
static const std::string Skippable_Type_Tag;
// Orca: usage-block builder markers (MACHINE_START_GCODE_END / MACHINE_END_GCODE_START /
// NOZZLE_CHANGE_START / NOZZLE_CHANGE_END / CP_TOOLCHANGE_WIPE), stored as static strings (the
// FLUSH/SKIPPABLE idiom above) rather than extending the Reserved_Tags arrays — these are
// multi-nozzle markers only ever emitted by BBL-printer paths. Public so the emission sites can
// reference them single-sourced. The MACHINE_*_GCODE_* emission (GCode.cpp, gated
// enable_pre_heating) activates the usage-block builder.
static const std::string Machine_Start_GCode_End_Tag;
static const std::string Machine_End_GCode_Start_Tag;
static const std::string Nozzle_Change_Start_Tag;
static const std::string Nozzle_Change_End_Tag;
static const std::string Toolchange_Wipe_Tag;
public:
enum class ETags : unsigned char
{
@@ -455,6 +595,9 @@ class Print;
EMoveType move_type{ EMoveType::Noop };
ExtrusionRole role{ erNone };
// SKIPPABLE tag classification stamped onto each time block. Feeds skippable_part_time
// and the injector's SKIPPABLE relocation. stNone unless inside a SKIPPABLE_* region.
SkipType skippable_type{ SkipType::stNone };
unsigned int move_id{ 0 };
unsigned int g1_line_id{ 0 };
unsigned int remaining_internal_g1_lines{ 0 };
@@ -624,6 +767,25 @@ class Print;
struct TimeProcessor
{
// Orca: the insert-line taxonomy + the ordered map of lines the pre-heat/pre-cool injector
// splices into the finished g-code, keyed by output-line id. Orca keeps its single-pass
// run_post_process (M73 / filament stats / ActualSpeedMove / Backtrace /
// machine_tool_change_time) intact and applies this map in a separate, gated ADDITIVE
// second file-rewrite pass (run_second_pass_injection); with an empty map that pass is a
// byte-for-byte identity rewrite. The map is populated by the PreCoolingInjector.
enum InsertLineType
{
PlaceholderReplace,
TimePredict,
FilamentChangePredict,
ExtruderChangePredict,
PreCooling,
PreHeating,
};
// first key is line id, second key is content
using InsertedLinesMap = std::map<unsigned int, std::vector<std::pair<std::string, InsertLineType>>>;
struct Planner
{
// Size of the firmware planner queue. The old 8-bit Marlins usually just managed 16 trapezoidal blocks.
@@ -651,6 +813,117 @@ class Print;
void reset();
};
// The pre-cool / pre-heat injection engine. It consumes the already-computed per-move time
// substrate (moves[i].time[valid_machine_id] / .gcode_id) and the first-pass usage blocks to
// locate idle-hotend windows, then emits M632/M400/M104/M633 lines into a
// TimeProcessor::InsertedLinesMap that the additive second file-rewrite pass
// (run_second_pass_injection) splices into the finished g-code. It is constructed and run ONLY
// when m_enable_pre_heating — single-nozzle printers (X1/P1/A1/H2S, flag false) never reach it.
// Every input is a const reference bundled from GCodeProcessor members; the injector never
// mutates GCodeProcessor state.
class PreCoolingInjector {
public:
struct ExtruderFreeBlock {
unsigned int free_lower_gcode_id;
unsigned int free_upper_gcode_id;
unsigned int partial_free_lower_id; // range of extrusion in wipe tower; without a wipe tower
unsigned int partial_free_upper_id; // partial_free lower/upper equal free_lower_gcode_id
int last_filament_id;
int next_filament_id;
int last_nozzle_id;
int next_nozzle_id;
int extruder_id; // partition key for the pre-heat/pre-cool region (extruder or hotend), not
// necessarily a real extruder id
bool ignore_cooling_before_tower = false;
};
void process_pre_cooling_and_heating(TimeProcessor::InsertedLinesMap& inserted_operation_lines);
void build_extruder_free_blocks(const std::vector<ExtruderPreHeating::FilamentUsageBlock>& filament_usage_blocks, const std::vector<ExtruderPreHeating::ExtruderUsageBlcok>& extruder_usage_blocks);
PreCoolingInjector(
const std::vector<GCodeProcessorResult::MoveVertex>& moves_,
const std::vector<std::string>& filament_types_,
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result_,
const std::vector<int>& filament_nozzle_temps_,
const std::vector<int>& filament_nozzle_temps_initial_layer_,
const std::vector<int>& physical_extruder_map_,
int valid_machine_id_,
float inject_time_threshold_,
bool handle_hotend_as_extruder_,
bool has_filament_switcher_,
const std::vector<int>& pre_cooling_temp_,
const std::vector<double>& cooling_rate_,
const std::vector<double>& heating_rate_,
const std::vector<std::pair<unsigned int, unsigned int>>& skippable_blocks_,
const std::vector<int>& extruder_max_nozzle_count_,
const std::vector<double>& filament_preheat_temperature_delta_,
const std::vector<double>& filament_max_temperature_drop_when_ec_,
unsigned int machine_start_gcode_end_id_,
unsigned int machine_end_gcode_start_id_,
const std::vector<ExtruderType>& extruder_types_,
const std::vector<double>& nozzle_diameter_
) :
moves(moves_),
filament_types(filament_types_),
nozzle_group_result(nozzle_group_result_),
filament_nozzle_temps(filament_nozzle_temps_),
filament_nozzle_temps_initial_layer(filament_nozzle_temps_initial_layer_),
physical_extruder_map(physical_extruder_map_),
valid_machine_id(valid_machine_id_),
inject_time_threshold(inject_time_threshold_),
handle_hotend_as_extruder(handle_hotend_as_extruder_),
has_filament_switcher(has_filament_switcher_),
filament_pre_cooling_temps(pre_cooling_temp_),
cooling_rate(cooling_rate_),
heating_rate(heating_rate_),
skippable_blocks(skippable_blocks_),
extruder_max_nozzle_count(extruder_max_nozzle_count_),
filament_preheat_temperature_delta(filament_preheat_temperature_delta_),
filament_max_temperature_drop_when_ec(filament_max_temperature_drop_when_ec_),
machine_start_gcode_end_id(machine_start_gcode_end_id_),
machine_end_gcode_start_id(machine_end_gcode_start_id_),
extruder_types(extruder_types_),
nozzle_diameter(nozzle_diameter_)
{
}
private:
std::vector<ExtruderFreeBlock> m_extruder_free_blocks;
const std::vector<GCodeProcessorResult::MoveVertex>& moves;
const std::vector<std::string>& filament_types;
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result;
const std::vector<int>& filament_nozzle_temps;
const std::vector<int>& filament_nozzle_temps_initial_layer;
const std::vector<int>& physical_extruder_map;
const int valid_machine_id;
const float inject_time_threshold;
const bool handle_hotend_as_extruder;
const bool has_filament_switcher;
const std::vector<double>& cooling_rate;
const std::vector<double>& heating_rate;
const std::vector<int>& filament_pre_cooling_temps; // target cooling temp during post extrusion
const std::vector<std::pair<unsigned int, unsigned int>>& skippable_blocks;
const std::vector<int>& extruder_max_nozzle_count;
const std::vector<double>& filament_preheat_temperature_delta;
const std::vector<double>& filament_max_temperature_drop_when_ec;
const unsigned int machine_start_gcode_end_id;
const unsigned int machine_end_gcode_start_id;
const std::vector<ExtruderType>& extruder_types;
const std::vector<double>& nozzle_diameter;
void inject_cooling_heating_command(
TimeProcessor::InsertedLinesMap& inserted_operation_lines,
const ExtruderFreeBlock& free_block,
float curr_temp,
float target_temp,
bool pre_cooling,
bool pre_heating
);
void build_by_filament_blocks(const std::vector<ExtruderPreHeating::FilamentUsageBlock>& filament_usage_blocks);
void build_by_extruder_blocks(const std::vector<ExtruderPreHeating::ExtruderUsageBlcok>& extruder_usage_blocks);
};
public:
class SeamsDetector
{
@@ -795,12 +1068,61 @@ class Print;
bool m_flushing; // mark a section with real flush
bool m_virtual_flushing; // mark a section with virtual flush, only for statistics
bool m_wipe_tower;
// Current-section SKIPPABLE state. Set by process_tags when inside a SKIPPABLE_* region;
// stamped onto each TimeBlock. The shipping time_lapse_gcode template emits SKIPPABLE_*
// widely, so these commonly go active (true / stTimelapse) and stamp blocks on most slices.
bool m_skippable{false};
SkipType m_skippable_type{SkipType::stNone};
int m_object_label_id{-1};
float m_print_z{0.0f};
std::vector<float> m_remaining_volume;
ExtruderTemps m_filament_nozzle_temp;
ExtruderTemps m_filament_nozzle_temp_first_layer;
std::vector<int> m_physical_extruder_map;
// Multi-nozzle context state. Per-extruder max (sub-)nozzle count; >1 marks a multi-nozzle
// extruder. Input for the pre-heat/filament-change-time injection model; not yet consumed by
// Orca's time estimator, so it is inert for existing printers.
std::vector<int> m_extruder_max_nozzle_count{1};
// Pre-heat / pre-cool injector estimator inputs. Populated from the config in apply_config
// (both overloads) and cleared in reset(), so the PreCoolingInjector has its inputs in place.
// Consumed only by the injector two-pass side-pass, gated on m_enable_pre_heating.
std::vector<std::string> m_filament_types;
std::vector<double> m_nozzle_diameter;
std::vector<double> m_hotend_cooling_rate{ 2.f };
std::vector<double> m_hotend_heating_rate{ 2.f };
std::vector<int> m_filament_pre_cooling_temp{ 0 };
std::vector<double> m_filament_preheat_temperature_delta;
bool m_enable_pre_heating{ false };
bool m_handle_hotend_as_extruder{ false };
bool m_has_filament_switcher{ false };
// [start,end] output-line-id ranges of each SKIPPABLE region, collected during
// run_post_process. The injector relocates pre-heat M104s out of these ranges. The shipping
// time_lapse_gcode template emits SKIPPABLE_* widely, so on a timelapse-on slice this is
// populated with many timelapse ranges (not empty) — the consumer must expect the common
// timelapse case, not only H2C/A2L wipe-tower ranges.
std::vector<std::pair<unsigned int, unsigned int>> m_skippable_blocks;
// First-pass usage blocks, built in run_post_process and stored on the member so the
// injector side-pass can consume them. Filled only when m_enable_pre_heating — single-nozzle
// printers (X1/P1/A1/H2S) never build them. They depend on the MACHINE_*_GCODE_* /
// NOZZLE_CHANGE_* emission the builder keys off.
std::vector<ExtruderPreHeating::FilamentUsageBlock> m_filament_blocks;
std::vector<ExtruderPreHeating::ExtruderUsageBlcok> m_extruder_blocks;
unsigned int m_machine_start_gcode_end_line_id{ (unsigned int) (-1) };
unsigned int m_machine_end_gcode_start_line_id{ (unsigned int) (-1) };
// Set when the MACHINE_END_GCODE_START tag is seen during the streaming parse; tells
// process_M400 to skip post-print end-gcode dwells (air purification, timelapse, sound)
// so they don't inflate the M73 estimate. BBS excludes them in calculate_time(is_final).
bool m_skip_end_gcode_delays{ false };
// Tracks, during the stream, which filament sits in each physical nozzle and which nozzle each
// extruder currently carries. Written by both branches of the two-arg process_filament_change
// (the fallback branch does occupancy bookkeeping only); read by the richer change-time model
// and by the per-slot machine-limit resolution. Single-nozzle printers never populate it.
MultiNozzleUtils::NozzleStatusRecorder m_nozzle_status_recorder;
// Nozzle grouping context for slot resolution during the streaming pass. Set before the
// replay begins (see initialize_from_context); deliberately separate from
// m_result.nozzle_group_result, which is handed over only after the stream for the
// pre-heat injector's second pass and gates the richer change-time model.
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> m_nozzle_group_result;
bool m_manual_filament_change;
//BBS: x, y offset for gcode generated
@@ -825,6 +1147,9 @@ class Print;
std::vector<unsigned char> m_last_filament_id;
std::vector<unsigned char> m_filament_id;
unsigned char m_extruder_id;
// Cached get_machine_config_idx() value; its inputs (active extruder + recorder occupancy)
// change only on filament-change events, where it is recomputed.
int m_machine_config_idx{0};
ExtruderColors m_extruder_colors;
ExtruderTemps m_extruder_temps;
bool m_is_XL_printer = false;
@@ -846,6 +1171,7 @@ class Print;
float m_preheat_time;
int m_preheat_steps;
bool m_disable_m73;
std::string m_printer_model;
enum class EProducer
{
@@ -875,6 +1201,11 @@ class Print;
public:
GCodeProcessor();
void init_filament_maps_and_nozzle_type_when_import_only_gcode();
// Reprocessing an already-generated g-code (from-previous / imported g-code) does not rebuild
// the per-filament nozzle grouping the multi-nozzle device GUI needs. Surface it onto the
// result: keep an already-seeded grouping (from initialize_from_context), otherwise synthesize
// a default one from the filament map so the result is never left without it.
void ensure_nozzle_group_result(int min_filament_count);
// check whether the gcode path meets the filament_map grouping requirements
bool check_multi_extruder_gcode_valid(const int extruder_size,
const Pointfs plate_printable_area,
@@ -886,6 +1217,11 @@ class Print;
const std::vector<std::set<int>>& unprintable_filament_types );
void apply_config(const PrintConfig& config);
void set_print(Print* print) { m_print = print; }
// Hand the nozzle grouping context to the estimator BEFORE the streaming replay, so the
// per-slot machine-limit resolution can follow the active nozzle. Null is fine (slot 0).
void initialize_from_context(const std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase>& nozzle_group_result) {
m_nozzle_group_result = nozzle_group_result;
}
DynamicConfig export_config_for_render() const;
@@ -1084,35 +1420,72 @@ class Print;
// Unload the current filament into the MK3 MMU2 unit at the end of print.
void process_M702(const GCodeReader::GCodeLine& line);
//Used for Elegoo printer to change tool head
void process_M6211(const GCodeReader::GCodeLine& line);
void process_elegoo_M6211(const GCodeReader::GCodeLine& line);
void process_SYNC(const GCodeReader::GCodeLine& line);
// Processes T line (Select Tool)
void process_T(const GCodeReader::GCodeLine& line);
void process_T(const std::string_view command);
// T variant carrying the H<nozzle> logical-nozzle id parsed off the command line. -1 = absent.
void process_T(const std::string_view command, int nozzle_id);
void process_M1020(const GCodeReader::GCodeLine &line);
void process_M622(const GCodeReader::GCodeLine &line);
void process_M623(const GCodeReader::GCodeLine &line);
void process_filament_change(int id);
// Richer hotend-change time model distinguishing extruder-switch / nozzle-in-extruder change /
// filament-in-nozzle change. Self-gated: for single-nozzle printers it delegates to
// process_filament_change(int) so their time estimate — hence exported g-code — is unchanged.
void process_filament_change(int id, int nozzle_id);
// Destination nozzle of a filament change: the explicit H<nozzle> id when given, else the
// filament's first nozzle in the grouping. Shared by the change-time model and the
// fallback-path occupancy bookkeeping.
std::optional<MultiNozzleUtils::NozzleInfo> resolve_target_nozzle(
const MultiNozzleUtils::NozzleGroupResultBase &group, int id, int nozzle_id) const;
// Machine slot of the nozzle currently mounted in the active extruder (0 when no grouping
// context / unknown extruder — the single-slot layout). Cached in m_machine_config_idx,
// recomputed on filament-change events.
int get_machine_config_idx() const;
// True only for multi-nozzle-capable printers (H2C cluster, or a dual/multi-extruder machine
// like H2D/X2D): the gate that admits the richer two-arg hotend-change time model. False for
// every single-extruder single-nozzle printer (X1/P1/A1/H2S/A2L).
bool use_multi_nozzle_change_time_model() const;
// post process the file with the given filename to:
// 1) add remaining time lines M73 and update moves' gcode ids accordingly
// 2) update used filament data
void run_post_process();
// Additive second file-rewrite pass. Splices the pre-heat/pre-cool injector's InsertedLinesMap
// into the finished g-code and re-shifts every move's gcode_id by the number of inserted lines
// before it. Runs only when m_enable_pre_heating, AFTER run_post_process, so single-nozzle
// printers (X1/P1/A1/H2S) never enter it; with an empty map it is a byte-for-byte identity rewrite.
void run_second_pass_injection();
// Shift each move's gcode_id by the count of injector lines inserted before it. No-op when the
// map is empty.
void handle_offsets_of_second_process(const TimeProcessor::InsertedLinesMap& inserted_operation_lines);
//BBS: different path_type is only used for arc move
void store_move_vertex(EMoveType type, EMovePathType path_type = EMovePathType::Noop_move, bool internal_only = false);
void set_extrusion_role(ExtrusionRole role);
// Resolve the SKIPPABLE_TYPE payload to a SkipType.
void set_skippable_type(const std::string_view type);
float minimum_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const;
float minimum_travel_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const;
// Machine limit arrays are indexed by time mode only: [0]=Normal, [1]=Stealth.
// Do NOT add an extruder_id parameter — OrcaSlicer does not use BambuStudio's
// per-nozzle machine limits (filament_map_2 / get_config_idx_for_filament).
// Speed/acceleration limit arrays are slot-major with two mode entries per machine slot:
// [slot*2 + mode], slot from get_machine_config_idx() (0 = the only slot on single-variant
// printers, whose arrays hold just [Normal, Stealth]). The 2-arg forms read slot 0 and stay
// exactly the historical mode-only lookup; jerk and the accelerations below are mode-only.
float get_axis_max_feedrate(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
float get_axis_max_feedrate(PrintEstimatedStatistics::ETimeMode mode, Axis axis, int machine_idx) const;
float get_axis_max_acceleration(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
float get_axis_max_acceleration(PrintEstimatedStatistics::ETimeMode mode, Axis axis, int machine_idx) const;
float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis, float acceleration) const;
float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
float get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
+435
View File
@@ -0,0 +1,435 @@
// Print-object ordering strategies: implementation.
// Consolidates TSP post-processing, Snake, and Best-of-Strategies.
#include "OrderingStrategies.hpp"
#include "../Geometry.hpp"
#include "../ShortestPath.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <numeric>
#include <unordered_map>
#include <utility>
#include <vector>
namespace Slic3r {
/* ====================================================================
* TSP post-processing utilities
* ==================================================================== */
bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_passes)
{
size_t pn = path.size();
if (pn <= 2) return false;
// Pre-compute edge lengths once per pass to avoid redundant norm() calls.
auto recompute_edges = [&]() {
std::vector<double> el(pn);
for (size_t i = 0; i < pn; ++i) {
size_t ni = (i + 1) % pn;
el[i] = (centers[path[i]].cast<double>() - centers[path[ni]].cast<double>()).norm();
}
return el;
};
std::vector<double> el = recompute_edges();
// Pre-compute squared edge lengths for early rejection in the inner loop.
auto recompute_edges_sq = [&]() {
std::vector<double> elsq(pn);
for (size_t i = 0; i < pn; ++i) {
size_t ni = (i + 1) % pn;
elsq[i] = (centers[path[i]].cast<double>() - centers[path[ni]].cast<double>()).squaredNorm();
}
return elsq;
};
std::vector<double> elsq = recompute_edges_sq();
bool improved = false;
for (int pass = 0; max_passes <= 0 || pass < max_passes; ++pass) {
size_t best_i = pn, best_j = pn;
double best_gain = 0;
for (size_t i = 0; i < pn; ++i) {
const Vec2d& pi = centers[path[i]].cast<double>();
const Vec2d& p_in = centers[path[(i + 1) % pn]].cast<double>();
double d_i = el[i];
double d_i_sq = elsq[i];
for (size_t j = i + 2; j < pn; ++j) {
size_t j_next = (j + 1) % pn;
// Skip the swap that would reverse the entire cycle (removes both
// edges (0,1) and (pn-1,0), equivalent to traversing the cycle backwards).
if (i == 0 && j_next == 0) continue;
const Vec2d& pj = centers[path[j]].cast<double>();
const Vec2d& p_jn = centers[path[j_next]].cast<double>();
double d_j = el[j];
// Early rejection using squared distances (avoids 2 sqrt calls).
double new_a_sq = (pj - pi).squaredNorm();
double new_b_sq = (p_jn - p_in).squaredNorm();
if (new_a_sq >= d_i_sq && new_b_sq >= elsq[j]) continue;
double new_a = std::sqrt(new_a_sq);
double new_b = std::sqrt(new_b_sq);
double gain = d_i + d_j - new_a - new_b;
if (gain > best_gain) {
best_gain = gain;
best_i = i; best_j = j;
}
}
}
if (best_i == pn) break;
improved = true;
// Reverse the best swap segment
std::reverse(path.begin() + best_i + 1, path.begin() + best_j + 1);
// Recompute edge lengths after reversal
el = recompute_edges();
elsq = recompute_edges_sq();
}
return improved;
}
// Fast bounding-box overlap test (rejects most non-intersecting pairs).
static inline bool bboxes_overlap(const Point& a, const Point& b, const Point& c, const Point& d)
{
return !(std::max(a.x(), b.x()) < std::min(c.x(), d.x()) ||
std::max(c.x(), d.x()) < std::min(a.x(), b.x()) ||
std::max(a.y(), b.y()) < std::min(c.y(), d.y()) ||
std::max(c.y(), d.y()) < std::min(a.y(), b.y()));
}
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
{
size_t pn = path.size();
if (pn <= 3) return false;
// Treat path as a cycle: include the closing edge (pn-1 -> 0), consistent with the other
// TSP helpers (2-opt, closing-edge rotation) that operate on the full cycle.
size_t n_edges = pn;
// Scan for first crossing; returns {i, j} or {npos, npos} if none.
auto find_crossing = [&]() -> std::pair<size_t, size_t> {
for (size_t i = 0; i < n_edges; ++i) {
const Point& ai = centers[path[i]];
const Point& bi = centers[path[(i + 1) % pn]];
for (size_t j = i + 2; j < n_edges; ++j) {
// Skip the (0, pn-1) pair: edges (0,1) and (pn-1,0) share node 0.
if (i == 0 && j == pn - 1) continue;
const Point& aj = centers[path[j]];
const Point& bj = centers[path[(j + 1) % pn]];
if (!bboxes_overlap(ai, bi, aj, bj)) continue;
if (Geometry::segments_intersect(ai, bi, aj, bj))
return {i, j};
}
}
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
};
// Process crossings one at a time: find first, reverse it, restart scan.
// Cap iterations to prevent infinite loops on collinear/overlapping segments.
int max_iters = static_cast<int>(pn * pn);
bool improved = false;
while (max_iters-- > 0) {
auto [ci, cj] = find_crossing();
if (ci == std::numeric_limits<size_t>::max()) break;
improved = true;
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
}
return improved;
}
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers)
{
size_t pn = path.size();
size_t best_start = 0;
double best_closing2 = std::numeric_limits<double>::max();
for (size_t start = 0; start < pn; ++start) {
size_t last = (start + pn - 1) % pn;
double d2 = (centers[path[start]].cast<double>() - centers[path[last]].cast<double>()).squaredNorm();
if (d2 < best_closing2) { best_closing2 = d2; best_start = start; }
}
std::rotate(path.begin(), path.begin() + best_start, path.end());
}
/* ====================================================================
* Snake ordering
* ==================================================================== */
struct SnakeRow { double avg_y; std::vector<size_t> indices; };
// --- Row threshold computation ---
// Extract unique Y values and use the median gap between them to determine
// the row threshold.
static double compute_row_threshold(const std::vector<double>& sorted_ys,
double y_min, double y_max,
size_t n,
double fraction_of_y_range,
double min_threshold_um)
{
constexpr double MIN_GAP_FILTER = 1.0; // ignore sub-micron gaps (coord_t = 1/100mm)
// Extract unique Y values
std::vector<double> unique_ys;
unique_ys.reserve(sorted_ys.size());
unique_ys.push_back(sorted_ys[0]);
for (size_t i = 1; i < sorted_ys.size(); ++i) {
if (sorted_ys[i] - sorted_ys[i - 1] > MIN_GAP_FILTER)
unique_ys.push_back(sorted_ys[i]);
}
double fallback_threshold = (y_max - y_min) * fraction_of_y_range;
if (unique_ys.size() <= 1) {
return std::max(fallback_threshold, min_threshold_um);
}
// Compute gaps between consecutive unique Y values
std::vector<double> gaps;
gaps.reserve(unique_ys.size() - 1);
for (size_t i = 1; i < unique_ys.size(); ++i)
gaps.push_back(unique_ys[i] - unique_ys[i - 1]);
if (gaps.empty()) {
return std::max(fallback_threshold, min_threshold_um);
}
// Sort gaps to find the median
std::sort(gaps.begin(), gaps.end());
double median_gap = gaps[gaps.size() / 2];
double min_gap = gaps.front();
// Threshold: half the gap between consecutive unique Y values.
double threshold = (median_gap < min_gap * 1.5) ? min_gap * 0.5 : median_gap * 0.5;
bool has_row_structure;
if (unique_ys.size() * 2 <= n) {
has_row_structure = true;
} else {
// Single-column or sparse: uniform gaps indicate a deliberate grid
double max_gap = *std::max_element(gaps.begin(), gaps.end());
has_row_structure = (max_gap < min_gap * 2.0);
}
if (has_row_structure) {
// For grid-like data, use the gap-based threshold directly.
return threshold;
}
return std::max(fallback_threshold, min_threshold_um);
}
// --- Row grouping ---
// Bin points into rows by quantising Y / threshold
static std::vector<SnakeRow> group_into_rows(const Points& centers, double row_threshold)
{
size_t n = centers.size();
std::unordered_map<int64_t, std::vector<size_t>> row_map;
for (size_t i = 0; i < n; ++i) {
int64_t y_key = static_cast<int64_t>(std::floor(static_cast<double>(centers[i].y()) / row_threshold));
row_map[y_key].push_back(i);
}
std::vector<SnakeRow> rows;
rows.reserve(row_map.size());
for (auto& [key, indices] : row_map) {
double avg_y = std::accumulate(indices.begin(), indices.end(), 0.0,
[&](double acc, size_t idx) { return acc + static_cast<double>(centers[idx].y()); })
/ indices.size();
rows.push_back({avg_y, std::move(indices)});
}
std::sort(rows.begin(), rows.end(),
[](const SnakeRow& a, const SnakeRow& b) { return a.avg_y < b.avg_y; });
return rows;
}
// Sort each row by X and greedily pick the direction (left->right or right->left)
// that minimises the transition distance from the previous row's endpoint.
static std::vector<size_t> build_serpentine_path(const Points& centers,
std::vector<SnakeRow>& rows)
{
std::vector<size_t> path;
path.reserve(centers.size());
for (size_t ri = 0; ri < rows.size(); ++ri) {
auto& row = rows[ri].indices;
std::sort(row.begin(), row.end(),
[&](size_t a, size_t b) { return centers[a].x() < centers[b].x(); });
if (ri == 0) {
path.insert(path.end(), row.begin(), row.end());
} else {
const Point& prev_end = centers[path.back()];
double dist_to_left = (prev_end.cast<double>() - centers[row.front()].cast<double>()).squaredNorm();
double dist_to_right = (prev_end.cast<double>() - centers[row.back()].cast<double>()).squaredNorm();
if (dist_to_left <= dist_to_right)
path.insert(path.end(), row.begin(), row.end());
else
path.insert(path.end(), row.rbegin(), row.rend());
}
}
return path;
}
// Row-based serpentine traversal: detect rows, bin points, snake through them.
static std::vector<size_t> row_serpentine_path(const Points& centers,
double fraction_of_y_range = 0.02,
double min_threshold_um = 1e4)
{
if (centers.empty()) return {};
size_t n = centers.size();
// Collect and sort Y coordinates.
std::vector<double> sorted_ys;
sorted_ys.reserve(n);
for (const auto& p : centers) sorted_ys.push_back(static_cast<double>(p.y()));
std::sort(sorted_ys.begin(), sorted_ys.end());
auto [ymin, ymax] = std::minmax_element(sorted_ys.begin(), sorted_ys.end());
double y_min = *ymin, y_max = *ymax;
double row_threshold = compute_row_threshold(sorted_ys, y_min, y_max, n,
fraction_of_y_range, min_threshold_um);
auto rows = group_into_rows(centers, row_threshold);
return build_serpentine_path(centers, rows);
}
std::vector<size_t> snake_core(const Points& centers)
{
if (centers.empty()) return {};
std::vector<size_t> path = row_serpentine_path(centers);
for (int iter = 0; iter < 3; ++iter) {
bool improved = tsp_2opt_improve(path, centers);
improved |= tsp_remove_crossings(path, centers);
if (!improved) break;
}
return path;
}
std::vector<const PrintInstance*> chain_print_object_instances_snake(const std::vector<const PrintObject*>& print_objects, const Point* start_near)
{
return chain_instances_with_core(print_objects, start_near, snake_core);
}
std::vector<const PrintInstance*> chain_print_object_instances_snake(const Print& print)
{
return chain_print_object_instances_snake(print.objects().vector(), nullptr);
}
/* ====================================================================
* Best-of-strategies meta-strategy
* ==================================================================== */
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const std::vector<const PrintObject*>& print_objects, const Point* start_near)
{
if (print_objects.empty())
return {};
// Run all strategies.
std::vector<std::vector<const PrintInstance*>> candidates;
candidates.push_back(chain_print_object_instances(print_objects, start_near));
candidates.push_back(chain_print_object_instances_snake(print_objects, start_near));
// Compute metrics for each candidate.
struct Candidate { double total_len; double max_edge; };
std::vector<Candidate> metrics;
metrics.reserve(candidates.size());
for (size_t i = 0; i < candidates.size(); ++i) {
double total = 0.0;
double mx = 0.0;
for (size_t j = 0; j < candidates[i].size(); ++j) {
size_t k = (j + 1) % candidates[i].size();
double d = (candidates[i][j]->shift.cast<double>() - candidates[i][k]->shift.cast<double>()).norm();
total += d;
if (d > mx) mx = d;
}
metrics.push_back({total, mx});
}
// Pick shortest total path; tiebreak on smallest max edge.
auto best_it = std::min_element(metrics.begin(), metrics.end(),
[](const Candidate& a, const Candidate& b) {
return a.total_len < b.total_len ||
(a.total_len == b.total_len && a.max_edge < b.max_edge);
});
size_t best = static_cast<size_t>(std::distance(metrics.begin(), best_it));
return candidates[best];
}
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const Print& print)
{
return chain_print_object_instances_best_of(print.objects().vector(), nullptr);
}
/* ====================================================================
* Island-level ordering entry point
* ==================================================================== */
std::vector<size_t> order_points_with_strategy(const Points& points, PrintOrder print_order, const Point* start_near)
{
if (points.empty())
return {};
if (print_order != PrintOrder::Snake && print_order != PrintOrder::BestOfStrategies)
// Nearest neighbor + post-processing; honours start_near natively.
return chain_points_with_postprocessing(points, start_near);
auto run_snake = [&points, start_near]() {
std::vector<size_t> path = snake_core(points);
if (start_near != nullptr && !path.empty()) {
// Start the cycle at the point closest to start_near.
size_t best_start = 0;
double best_d2 = std::numeric_limits<double>::max();
for (size_t k = 0; k < points.size(); ++k) {
double d2 = (points[k].cast<double>() - start_near->cast<double>()).squaredNorm();
if (d2 < best_d2) { best_d2 = d2; best_start = k; }
}
auto it = std::find(path.begin(), path.end(), best_start);
if (it != path.begin() && it != path.end())
std::rotate(path.begin(), it, path.end());
} else {
tsp_rotate_minimize_closing(path, points);
}
return path;
};
if (print_order == PrintOrder::Snake)
return run_snake();
// Best-of: pick the shortest total cycle; tiebreak on smallest max edge.
std::vector<std::vector<size_t>> candidates;
candidates.emplace_back(chain_points_with_postprocessing(points, start_near));
candidates.emplace_back(run_snake());
size_t best = 0;
double best_len = std::numeric_limits<double>::max();
double best_edge = std::numeric_limits<double>::max();
for (size_t i = 0; i < candidates.size(); ++i) {
double len = tsp_cycle_path_length(candidates[i], points);
double edge = tsp_max_edge_length(candidates[i], points);
if (len < best_len || (len == best_len && edge < best_edge)) {
best_len = len; best_edge = edge; best = i;
}
}
return candidates[best];
}
} // namespace Slic3r
+148
View File
@@ -0,0 +1,148 @@
// Print-object ordering strategies and shared TSP post-processing utilities.
#ifndef slic3r_OrderingStrategies_hpp_
#define slic3r_OrderingStrategies_hpp_
#include "../libslic3r.h"
#include "../Point.hpp"
#ifndef SLIC3R_TEST_HARNESS
#include "../Print.hpp"
#endif
#include <algorithm>
#include <limits>
#include <utility>
#include <vector>
namespace Slic3r {
// --- Path improvement (operate on index vectors into `centers`) ---
// 2-opt improvement: reverses segments that reduce total cycle path length.
// Returns true if any improvement was made.
bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_passes = 10);
// Crossing removal: reverse any segment pair whose edges geometrically cross.
// Returns true if any crossing was removed.
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers);
// Rotate the cycle so the closing edge (last -> first) is minimized.
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers);
// Total Euclidean path length of a cycle (including closing edge).
inline double tsp_cycle_path_length(const std::vector<size_t>& path, const Points& centers)
{
if (path.size() < 2) return 0.0;
double total = 0.0;
for (size_t i = 0; i < path.size(); ++i) {
size_t next = (i + 1) % path.size();
total += (centers[path[i]].cast<double>() - centers[path[next]].cast<double>()).norm();
}
return total;
}
// Maximum edge length of a cycle (including closing edge).
inline double tsp_max_edge_length(const std::vector<size_t>& path, const Points& centers)
{
if (path.size() < 2) return 0.0;
double mx = 0.0;
for (size_t i = 0; i < path.size(); ++i) {
size_t next = (i + 1) % path.size();
double d = (centers[path[i]].cast<double>() - centers[path[next]].cast<double>()).norm();
if (d > mx) mx = d;
}
return mx;
}
#ifndef SLIC3R_TEST_HARNESS
// --- Wrapper boilerplate ---
// Collect instance centers from PrintObjects, optionally pre-rotate to honour
// start_near, call a core algorithm, and map the result back to PrintInstance*.
template<typename CoreFn>
std::vector<const PrintInstance*> chain_instances_with_core(
const std::vector<const PrintObject*>& print_objects,
const Point* start_near,
CoreFn&& core_fn)
{
Points instance_centers;
std::vector<std::pair<size_t, size_t>> instances;
for (size_t i = 0; i < print_objects.size(); ++i) {
const PrintObject& object = *print_objects[i];
for (size_t j = 0; j < object.instances().size(); ++j) {
instance_centers.emplace_back(object.instances()[j].shift);
instances.emplace_back(i, j);
}
}
if (instance_centers.empty()) return {};
// If start_near is provided, pre-rotate so closest point is first.
if (start_near != nullptr) {
size_t best_start = 0;
double best_d2 = std::numeric_limits<double>::max();
for (size_t k = 0; k < instance_centers.size(); ++k) {
double d2 = (instance_centers[k].cast<double>() - start_near->cast<double>()).squaredNorm();
if (d2 < best_d2) { best_d2 = d2; best_start = k; }
}
std::rotate(instance_centers.begin(), instance_centers.begin() + best_start, instance_centers.end());
std::rotate(instances.begin(), instances.begin() + best_start, instances.end());
}
auto path = core_fn(instance_centers);
// Rotate the cycle so the first element is the best starting point.
// When start_near is provided, pick the point closest to it (preserving
// the pre-rotation). Otherwise minimise the closing edge.
if (start_near != nullptr && !path.empty()) {
// Pre-rotation already put the closest point at index 0.
// Find where index 0 appears in the path and rotate it to the front.
auto it = std::find(path.begin(), path.end(), size_t(0));
if (it != path.begin())
std::rotate(path.begin(), it, path.end());
} else {
tsp_rotate_minimize_closing(path, instance_centers);
}
std::vector<const PrintInstance*> out;
out.reserve(path.size());
for (size_t step : path) {
out.emplace_back(&print_objects[instances[step].first]->instances()[instances[step].second]);
}
return out;
}
#endif // SLIC3R_TEST_HARNESS
// --- Core algorithms (operate on raw Points, return index permutations) ---
// Snake ordering: row grouping + serpentine traversal + post-processing.
std::vector<size_t> snake_core(const Points& centers);
#ifndef SLIC3R_TEST_HARNESS
// --- Production wrappers ---
// Snake ordering.
std::vector<const PrintInstance*> chain_print_object_instances_snake(const std::vector<const PrintObject*>& print_objects, const Point* start_near);
std::vector<const PrintInstance*> chain_print_object_instances_snake(const Print& print);
// Best-of-strategies: run all strategies and return the shortest result.
// Primary: shortest total path; secondary tiebreaker: smallest max edge.
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const std::vector<const PrintObject*>& print_objects, const Point* start_near);
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const Print& print);
// Order raw points with the selected strategy, returning an index permutation. Island-level
// counterpart of the chain_print_object_instances_* helpers. The returned cycle starts at the
// point closest to start_near; orders without a dedicated strategy use nearest-neighbor chaining.
std::vector<size_t> order_points_with_strategy(const Points& points, PrintOrder print_order, const Point* start_near);
#endif // SLIC3R_TEST_HARNESS
} // namespace Slic3r
#endif /* slic3r_OrderingStrategies_hpp_ */
-379
View File
@@ -1,379 +0,0 @@
#include "PostProcessor.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/format.hpp"
#include "libslic3r/I18N.hpp"
#include <boost/algorithm/string.hpp>
#include <boost/log/trivial.hpp>
#include <boost/format.hpp>
#include <boost/filesystem.hpp>
#include <boost/nowide/cstdlib.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/nowide/fstream.hpp>
// BBS
#include <iostream>
#include <fstream>
#ifdef WIN32
// The standard Windows includes.
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
#include <shellapi.h>
// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
// This routine appends the given argument to a command line such that CommandLineToArgvW will return the argument string unchanged.
// Arguments in a command line should be separated by spaces; this function does not add these spaces.
// Argument - Supplies the argument to encode.
// CommandLine - Supplies the command line to which we append the encoded argument string.
static void quote_argv_winapi(const std::wstring &argument, std::wstring &commmand_line_out)
{
// Don't quote unless we actually need to do so --- hopefully avoid problems if programs won't parse quotes properly.
if (argument.empty() == false && argument.find_first_of(L" \t\n\v\"") == argument.npos)
commmand_line_out.append(argument);
else {
commmand_line_out.push_back(L'"');
for (auto it = argument.begin(); ; ++ it) {
unsigned number_backslashes = 0;
while (it != argument.end() && *it == L'\\') {
++ it;
++ number_backslashes;
}
if (it == argument.end()) {
// Escape all backslashes, but let the terminating double quotation mark we add below be interpreted as a metacharacter.
commmand_line_out.append(number_backslashes * 2, L'\\');
break;
} else if (*it == L'"') {
// Escape all backslashes and the following double quotation mark.
commmand_line_out.append(number_backslashes * 2 + 1, L'\\');
commmand_line_out.push_back(*it);
} else {
// Backslashes aren't special here.
commmand_line_out.append(number_backslashes, L'\\');
commmand_line_out.push_back(*it);
}
}
commmand_line_out.push_back(L'"');
}
}
static DWORD execute_process_winapi(const std::wstring &command_line)
{
// Extract the current environment to be passed to the child process.
std::wstring envstr;
{
wchar_t *env = GetEnvironmentStrings();
assert(env != nullptr);
const wchar_t* var = env;
size_t totallen = 0;
size_t len;
while ((len = wcslen(var)) > 0) {
totallen += len + 1;
var += len + 1;
}
envstr = std::wstring(env, totallen);
FreeEnvironmentStrings(env);
}
STARTUPINFOW startup_info;
memset(&startup_info, 0, sizeof(startup_info));
startup_info.cb = sizeof(STARTUPINFO);
#if 0
startup_info.dwFlags = STARTF_USESHOWWINDOW;
startup_info.wShowWindow = SW_HIDE;
#endif
PROCESS_INFORMATION process_info;
if (! ::CreateProcessW(
nullptr /* lpApplicationName */, (LPWSTR)command_line.c_str(), nullptr /* lpProcessAttributes */, nullptr /* lpThreadAttributes */, false /* bInheritHandles */,
CREATE_UNICODE_ENVIRONMENT /* | CREATE_NEW_CONSOLE */ /* dwCreationFlags */, (LPVOID)envstr.c_str(), nullptr /* lpCurrentDirectory */, &startup_info, &process_info))
throw Slic3r::RuntimeError(std::string("Failed starting the script ") + boost::nowide::narrow(command_line) + ", Win32 error: " + std::to_string(int(::GetLastError())));
::WaitForSingleObject(process_info.hProcess, INFINITE);
ULONG rc = 0;
::GetExitCodeProcess(process_info.hProcess, &rc);
::CloseHandle(process_info.hThread);
::CloseHandle(process_info.hProcess);
return rc;
}
// Run the script. If it is a perl script, run it through the bundled perl interpreter.
// If it is a batch file, run it through the cmd.exe.
// Otherwise run it directly.
static int run_script(const std::string &script, const std::string &gcode, std::string &/*std_err*/)
{
// Unpack the argument list provided by the user.
int nArgs;
LPWSTR *szArglist = CommandLineToArgvW(boost::nowide::widen(script).c_str(), &nArgs);
if (szArglist == nullptr || nArgs <= 0) {
// CommandLineToArgvW failed. Maybe the command line escapment is invalid?
throw Slic3r::RuntimeError(std::string("Post processing script ") + script + " on file " + gcode + " failed. CommandLineToArgvW() refused to parse the command line path.");
}
std::wstring command_line;
std::wstring command = szArglist[0];
if (! boost::filesystem::exists(boost::filesystem::path(command)))
throw Slic3r::RuntimeError(std::string("The configured post-processing script does not exist: ") + boost::nowide::narrow(command));
if (boost::iends_with(command, L".pl")) {
// This is a perl script. Run it through the perl interpreter.
// The current process may be slic3r.exe or slic3r-console.exe.
// Find the path of the process:
wchar_t wpath_exe[_MAX_PATH + 1];
::GetModuleFileNameW(nullptr, wpath_exe, _MAX_PATH);
boost::filesystem::path path_exe(wpath_exe);
boost::filesystem::path path_perl = path_exe.parent_path() / "perl" / "perl.exe";
if (! boost::filesystem::exists(path_perl)) {
LocalFree(szArglist);
throw Slic3r::RuntimeError(std::string("Perl interpreter ") + path_perl.string() + " does not exist.");
}
// Replace it with the current perl interpreter.
quote_argv_winapi(boost::nowide::widen(path_perl.string()), command_line);
command_line += L" ";
} else if (boost::iends_with(command, ".bat")) {
// Run a batch file through the command line interpreter.
command_line = L"cmd.exe /C ";
}
for (int i = 0; i < nArgs; ++ i) {
quote_argv_winapi(szArglist[i], command_line);
command_line += L" ";
}
LocalFree(szArglist);
quote_argv_winapi(boost::nowide::widen(gcode), command_line);
return (int)execute_process_winapi(command_line);
}
#else
// POSIX
#include <cstdlib> // getenv()
#include <sstream>
#include <boost/process.hpp>
namespace process = boost::process;
static int run_script(const std::string &script, const std::string &gcode, std::string &std_err)
{
// Try to obtain user's default shell
const char *shell = ::getenv("SHELL");
if (shell == nullptr) { shell = "/bin/sh"; }
// Quote and escape the gcode path argument
std::string command { script };
command.append(" '");
for (char c : gcode) {
if (c == '\'') { command.append("'\\''"); }
else { command.push_back(c); }
}
command.push_back('\'');
BOOST_LOG_TRIVIAL(debug) << boost::format("Executing script, shell: %1%, command: %2%") % shell % command;
process::ipstream istd_err;
process::child child(shell, "-c", command, process::std_err > istd_err);
std_err.clear();
std::string line;
while (child.running() && std::getline(istd_err, line)) {
std_err.append(line);
std_err.push_back('\n');
}
child.wait();
return child.exit_code();
}
#endif
namespace Slic3r {
//! macro used to mark string used at localization,
//! return same string
#define L(s) (s)
#define _(s) Slic3r::I18N::translate(s)
// BBS
void gcode_add_line_number(const std::string& path, const DynamicPrintConfig& config)
{
const ConfigOptionBool* opt = config.opt<ConfigOptionBool>("gcode_add_line_number");
if (!opt->getBool())
return;
auto gcode_file = boost::filesystem::path(path);
if (!boost::filesystem::exists(gcode_file))
return;
std::fstream fs;
std::string new_gcode;
fs.open(gcode_file.c_str(), std::fstream::in | std::fstream::out);
size_t line_number = 1;
std::string gcode_line;
while (std::getline(fs, gcode_line)) {
char num_str[128];
memset(num_str, 0, sizeof(num_str));
snprintf(num_str, sizeof(num_str), "%zd", line_number);
new_gcode += std::string("N") + num_str + " " + gcode_line + "\n";
line_number++;
}
fs.clear();
fs.seekp(0, std::ios_base::beg);
fs.write(new_gcode.c_str(), new_gcode.length());
fs.close();
}
// Run post processing script / scripts if defined.
// Returns true if a post-processing script was executed.
// Returns false if no post-processing script was defined.
// Throws an exception on error.
// host is one of "File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ...
// For a "File" target, a temp file will be created for src_path by adding a ".pp" suffix and src_path will be updated.
// In that case the caller is responsible to delete the temp file created.
// output_name is the final name of the G-code on SD card or when uploaded to PrusaLink or OctoPrint.
// If uploading to PrusaLink or OctoPrint, then the file will be renamed to output_name first on the target host.
// The post-processing script may change the output_name.
bool run_post_process_scripts(std::string &src_path, bool make_copy, const std::string &host, std::string &output_name, const DynamicPrintConfig &config)
{
const auto *post_process = config.opt<ConfigOptionStrings>("post_process");
if (// likely running in SLA mode
post_process == nullptr ||
// no post-processing script
post_process->values.empty())
return false;
std::string path;
if (make_copy) {
// Don't run the post-processing script on the input file, it will be memory mapped by the G-code viewer.
// Make a copy.
path = src_path + ".pp";
// First delete an old file if it exists.
try {
if (boost::filesystem::exists(path))
boost::filesystem::remove(path);
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << Slic3r::format("Failed deleting an old temporary file %1% before running a post-processing script: %2%", path, err.what());
}
// Second make a copy.
std::string error_message;
if (copy_file(src_path, path, error_message, false) != SUCCESS)
throw Slic3r::RuntimeError(Slic3r::format("Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%", src_path, error_message));
} else {
// Don't make a copy of the G-code before running the post-processing script.
path = src_path;
}
auto delete_copy = [&path, &src_path, make_copy]() {
if (make_copy)
try {
if (boost::filesystem::exists(path))
boost::filesystem::remove(path);
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << Slic3r::format("Failed deleting a temporary copy %1% of a G-code file %2% : %3%", path, src_path, err.what());
}
};
auto gcode_file = boost::filesystem::path(path);
if (! boost::filesystem::exists(gcode_file))
throw Slic3r::RuntimeError(std::string("Post-processor can't find exported gcode file"));
// Store print configuration into environment variables.
config.setenv_();
// Let the post-processing script know the target host ("File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ...)
boost::nowide::setenv("SLIC3R_PP_HOST", host.c_str(), 1);
// Let the post-processing script know the final file name. For "File" host, it is a full path of the target file name and its location, for example pointing to an SD card.
// For "PrusaLink" or "OctoPrint", it is a file name optionally with a directory on the target host.
boost::nowide::setenv("SLIC3R_PP_OUTPUT_NAME", output_name.c_str(), 1);
// Path to an optional file that the post-processing script may create and populate it with a single line containing the output_name replacement.
std::string path_output_name = path + ".output_name";
auto remove_output_name_file = [&path_output_name, &src_path]() {
try {
if (boost::filesystem::exists(path_output_name))
boost::filesystem::remove(path_output_name);
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << Slic3r::format("Failed deleting a file %1% carrying the final name / path of a G-code file %2%: %3%", path_output_name, src_path, err.what());
}
};
// Remove possible stalled path_output_name of the previous run.
remove_output_name_file();
try {
for (const std::string &scripts : post_process->values) {
std::vector<std::string> lines;
boost::split(lines, scripts, boost::is_any_of("\r\n"));
for (std::string script : lines) {
// Ignore empty post processing script lines.
boost::trim(script);
if (script.empty())
continue;
BOOST_LOG_TRIVIAL(info) << "Executing script " << script << " on file " << path;
std::string std_err;
const int result = run_script(script, gcode_file.string(), std_err);
if (result != 0) {
const std::string msg = std_err.empty() ? (boost::format("Post-processing script %1% on file %2% failed.\nError code: %3%") % script % path % result).str()
: (boost::format("Post-processing script %1% on file %2% failed.\nError code: %3%\nOutput:\n%4%") % script % path % result % std_err).str();
BOOST_LOG_TRIVIAL(error) << msg;
delete_copy();
throw Slic3r::RuntimeError(msg);
}
if (! boost::filesystem::exists(gcode_file)) {
const std::string msg = (boost::format(_(L(
"Post-processing script %1% failed.\n\n"
"The post-processing script is expected to change the G-code file %2% in place, but the G-code file was deleted and likely saved under a new name.\n"
"Please adjust the post-processing script to change the G-code in place and consult the manual on how to optionally rename the post-processed G-code file.\n")))
% script % path).str();
BOOST_LOG_TRIVIAL(error) << msg;
throw Slic3r::RuntimeError(msg);
}
}
}
if (boost::filesystem::exists(path_output_name)) {
try {
// Read a single line from path_output_name, which should contain the new output name of the post-processed G-code.
boost::nowide::fstream f;
f.open(path_output_name, std::ios::in);
std::string new_output_name;
std::getline(f, new_output_name);
f.close();
if (host == "File") {
namespace fs = boost::filesystem;
fs::path op(new_output_name);
if (op.is_relative() && op.has_filename() && op.parent_path().empty()) {
// Is this just a filename? Make it an absolute path.
auto outpath = fs::path(output_name).parent_path();
outpath /= op.string();
new_output_name = outpath.string();
}
else {
if (! op.is_absolute() || ! op.has_filename())
throw Slic3r::RuntimeError("Unable to parse desired new path from output name file");
}
if (! fs::exists(fs::path(new_output_name).parent_path()))
throw Slic3r::RuntimeError(Slic3r::format("Output directory does not exist: %1%",
fs::path(new_output_name).parent_path().string()));
}
BOOST_LOG_TRIVIAL(trace) << "Post-processing script changed the file name from " << output_name << " to " << new_output_name;
output_name = new_output_name;
} catch (const std::exception &err) {
throw Slic3r::RuntimeError(Slic3r::format("run_post_process_scripts: Failed reading a file %1% "
"carrying the final name / path of a G-code file: %2%",
path_output_name, err.what()));
}
remove_output_name_file();
}
} catch (...) {
remove_output_name_file();
delete_copy();
throw;
}
src_path = std::move(path);
return true;
}
} // namespace Slic3r
-34
View File
@@ -1,34 +0,0 @@
#ifndef slic3r_GCode_PostProcessor_hpp_
#define slic3r_GCode_PostProcessor_hpp_
#include <string>
#include "../libslic3r.h"
#include "../PrintConfig.hpp"
namespace Slic3r {
// Run post processing script / scripts if defined.
// Returns true if a post-processing script was executed.
// Returns false if no post-processing script was defined.
// Throws an exception on error.
// host is one of "File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ...
// If make_copy, then a temp file will be created for src_path by adding a ".pp" suffix and src_path will be updated.
// In that case the caller is responsible to delete the temp file created.
// output_name is the final name of the G-code on SD card or when uploaded to PrusaLink or OctoPrint.
// If uploading to PrusaLink or OctoPrint, then the file will be renamed to output_name first on the target host.
// The post-processing script may change the output_name.
extern bool run_post_process_scripts(std::string &src_path, bool make_copy, const std::string &host, std::string &output_name, const DynamicPrintConfig &config);
inline bool run_post_process_scripts(std::string &src_path, const DynamicPrintConfig &config)
{
std::string src_path_name = src_path;
return run_post_process_scripts(src_path, false, "File", src_path_name, config);
}
// BBS
extern void gcode_add_line_number(const std::string &path, const DynamicPrintConfig &config);
} // namespace Slic3r
#endif /* slic3r_GCode_PostProcessor_hpp_ */
+2 -1
View File
@@ -143,7 +143,8 @@ BoundingBoxf get_wipe_tower_extrusions_extents(const Print &print, const coordf_
double wipe_tower_y = print.config().wipe_tower_y.get_at(plate_idx) + plate_origin(1);
Transform2d trafo =
Eigen::Translation2d(wipe_tower_x, wipe_tower_y) *
Eigen::Rotation2Dd(Geometry::deg2rad(print.config().wipe_tower_rotation_angle.value));
Eigen::Rotation2Dd(Geometry::deg2rad(print.config().wipe_tower_rotation_angle.value)) *
Eigen::Translation2d(print.wipe_tower_data().rib_offset.cast<double>()); // tower-local rib-wall shift, zero unless rib
BoundingBoxf bbox;
for (const std::vector<WipeTower::ToolChangeResult> &tool_changes : print.wipe_tower_data().tool_changes) {
+54 -3
View File
@@ -361,7 +361,8 @@ namespace Slic3r {
* @param safe_areas A collection of extended polygons defining the safe areas.
* @return Point The nearest point within the safe areas or the default timelapse position if no safe areas exist.
*/
Point pick_pos_internal(const Point& curr_pos, const ExPolygons& safe_areas, const ExPolygons& path_collision_area, bool detect_path_collision)
Point pick_pos_internal(const Point& curr_pos, const ExPolygons& safe_areas, const ExPolygons& path_collision_area, bool detect_path_collision,
const std::optional<Point>& farthest_point = std::nullopt)
{
struct CandidatePoint
{
@@ -381,7 +382,11 @@ namespace Slic3r {
std::priority_queue<CandidatePoint> max_heap;
const double candidate_point_segment = scale_(5), weight_of_camera=1./3.;
auto penaltyFunc = [&weight_of_camera](const Point &curr_post, const Point &CameraPos, const Point &candidatet) -> double {
auto penaltyFunc = [&weight_of_camera, &farthest_point](const Point &curr_post, const Point &CameraPos, const Point &candidatet) -> double {
if (farthest_point.has_value()) {
// Farthest-point timelapse: prefer candidate closest to the farthest point (L1 norm)
return (farthest_point.value() - candidatet).cwiseAbs().sum();
}
// move distance + Camera occlusion penalty function
double ret_pen = (curr_post - candidatet).cwiseAbs().sum() - weight_of_camera * (CameraPos - candidatet).cwiseAbs().sum();
return ret_pen;
@@ -523,7 +528,7 @@ namespace Slic3r {
path_collision_area = union_ex(layer_slices_without_curr, rod_limit_areas);
}
return pick_pos_internal(center_p, safe_area,path_collision_area, by_object);
return pick_pos_internal(center_p, safe_area,path_collision_area, by_object, ctx.farthest_point);
}
/**
@@ -610,4 +615,50 @@ namespace Slic3r {
return *m_all_layer_pos;
}
// Whether the head can travel to X0 without crossing any other instance that is
// taller than the current print position.
bool TimelapsePosPicker::get_is_clear_to_x0(const PosPickCtx &ctx)
{
bool by_object = m_print_seq == PrintSequence::ByObject;
std::vector<const PrintObject *> object_list = get_object_list(ctx.printed_objects);
auto range_intersect = [](int left1, int right1, int left2, int right2) {
if (left1 <= left2 && left2 <= right1) return true;
if (left2 <= left1 && left1 <= right2) return true;
return false;
};
ExPolygons unclear_area;
const Layer *layer = ctx.curr_layer;
float z_target = layer->print_z;
float z_low = layer->print_z - 0.5;
float z_high = layer->print_z + 0.5;
for (auto &obj : object_list) {
for (auto &instance : obj->instances()) {
auto instance_bbox = get_real_instance_bbox(instance);
bool is_curr_obj = ( obj == object_list.back() ) || ( !by_object ),
higher_than_curr_pos = instance_bbox.max.z() > z_target;
if (!is_curr_obj && range_intersect(instance_bbox.min.z(), instance_bbox.max.z(), z_low, z_high)) {
ExPolygon expoly;
expoly.contour = {{scale_(instance_bbox.min.x()), scale_(instance_bbox.min.y())},
{scale_(instance_bbox.max.x()), scale_(instance_bbox.min.y())},
{scale_(instance_bbox.max.x()), scale_(instance_bbox.max.y())},
{scale_(instance_bbox.min.x()), scale_(instance_bbox.max.y())}};
expoly.contour = expand_object_projection(expoly.contour, by_object, higher_than_curr_pos);
unclear_area.emplace_back(std::move(expoly));
}
}
}
Point curr_pos_in_plate = {ctx.curr_pos.x() - scale_(m_plate_offset.x()), ctx.curr_pos.y() - scale_(m_plate_offset.y())};
for (const ExPolygon &expoly : unclear_area) {
BoundingBox bbox = expoly.contour.bounding_box();
if (curr_pos_in_plate.y() < bbox.min.y() || curr_pos_in_plate.y() > bbox.max.y()) continue;
if (bbox.min.x() <= curr_pos_in_plate.x()) return false;
}
return true;
}
}
@@ -22,6 +22,9 @@ namespace Slic3r {
int picture_extruder_id; // the extruder id to take picture
int curr_extruder_id;
std::optional<std::vector<const PrintObject*>> printed_objects; // printed objects, only have value in by object mode
// Farthest-point timelapse: plate-relative scaled point; when set, pick_pos_internal
// biases the picked snapshot position toward this point (nullopt → legacy camera-occlusion loss).
std::optional<Point> farthest_point;
};
// data are stored without plate offset
@@ -32,6 +35,9 @@ namespace Slic3r {
~TimelapsePosPicker() = default;
Point pick_pos(const PosPickCtx& ctx);
// Is the path to X0 clear of other (taller) instances? Drives the
// `clear_to_x0` timelapse-gcode variable (g39 clamping detection).
bool get_is_clear_to_x0(const PosPickCtx& ctx);
void init(const Print* print, const Point& plate_offset);
void reset();
private:
+721 -20
View File
@@ -7,13 +7,100 @@
namespace Slic3r
{
// ==================== MaxFlowWithLowerBounds ====================
struct MaxFlowWithLowerBounds {
public:
void add_edge(int from, int to, int capacity);
bool bfs();
int dfs(int u, int f);
int solve(std::vector<int>& matching);
public:
std::vector<int> l_nodes;
std::vector<int> r_nodes;
std::vector<Edge> edges;
std::vector<std::vector<int>> adj;
std::vector<int> level;
std::vector<int> it;
int total_nodes{ -1 };
int source_id{ -1 };
int sink_id{ -1 };
};
void MaxFlowWithLowerBounds::add_edge(int from, int to, int capacity)
{
adj[from].emplace_back(edges.size());
edges.emplace_back(from, to, capacity, 0);
// also add the reverse residual edge with zero capacity
adj[to].emplace_back(edges.size());
edges.emplace_back(to, from, 0, 0);
}
bool MaxFlowWithLowerBounds::bfs() {
level.assign(total_nodes, -1);
std::queue<int> q;
q.push(source_id);
level[source_id] = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int eid : adj[u]) {
Edge &e = edges[eid];
if (e.flow < e.capacity && level[e.to] == -1) {
level[e.to] = level[u] + 1;
q.push(e.to);
}
}
}
return level[sink_id] != -1;
}
int MaxFlowWithLowerBounds::dfs(int u, int f) {
if (u == sink_id) return f;
for (int &i = it[u]; i < (int)adj[u].size(); ++i) {
int eid = adj[u][i];
Edge &e = edges[eid];
if (e.flow < e.capacity && level[e.to] == level[u] + 1) {
int pushed = dfs(e.to, std::min(f, e.capacity - e.flow));
if (pushed > 0) {
e.flow += pushed;
edges[eid ^ 1].flow -= pushed;
return pushed;
}
}
}
return 0;
}
int MaxFlowWithLowerBounds::solve(std::vector<int>& matching) {
int flow = 0;
while (bfs()) {
it.assign(total_nodes, 0);
while (int pushed = dfs(source_id, MaxFlowGraph::INF))
flow += pushed;
}
int L = l_nodes.size();
int R = r_nodes.size();
// collect l-r matches
matching.resize(l_nodes.size(), MaxFlowGraph::INVALID_ID);
for (int u = 0; u < L; ++u) {
for (int eid : adj[u]) {
Edge &e = edges[eid];
if (e.flow > 0 && e.to >= L && e.to < L + R) {
matching[e.from] = e.to - L;
}
}
}
return flow;
}
// ==================== MinCostMaxFlow ====================
struct MinCostMaxFlow {
public:
struct Edge {
int from, to, capacity, cost, flow;
Edge(int u, int v, int cap, int cst) : from(u), to(v), capacity(cap), cost(cst), flow(0) {}
};
std::vector<int> solve();
void add_edge(int from, int to, int capacity, int cost);
bool spfa(int source, int sink);
@@ -107,15 +194,10 @@ namespace Slic3r
{
if (l_nodes[idx_in_left] == -1) {
return 0;
//TODO: test more here
int sum = 0;
for (int i = 0; i < matrix.size(); ++i)
sum += matrix[i][idx_in_right];
sum /= matrix.size();
return -sum;
}
return matrix[l_nodes[idx_in_left]][r_nodes[idx_in_right]];
float val = matrix[l_nodes[idx_in_left]][r_nodes[idx_in_right]];
return std::min(static_cast<int>(val), MaxFlowGraph::MCMF_MAX_EDGE_COST);
}
@@ -123,27 +205,40 @@ namespace Slic3r
const std::unordered_map<int, std::vector<int>>& uv_link_limits,
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits,
const std::vector<int>& u_capacity,
const std::vector<int>& v_capacity)
const std::vector<int>& v_capacity,
const std::vector<std::pair<std::set<int>,int>>& v_group_capacity)
{
assert(u_capacity.empty() || u_capacity.size() == u_nodes.size());
assert(v_capacity.empty() || v_capacity.size() == v_nodes.size());
l_nodes = u_nodes;
r_nodes = v_nodes;
total_nodes = u_nodes.size() + v_nodes.size() + 2;
total_nodes = u_nodes.size() + v_nodes.size() + v_group_capacity.size() + 2;
source_id = total_nodes - 2;
sink_id = total_nodes - 1;
adj.resize(total_nodes);
std::vector<int>v_node_to(v_nodes.size(), sink_id);
for (size_t gid = 0; gid < v_group_capacity.size(); ++gid) {
for (auto vid : v_group_capacity[gid].first)
v_node_to[vid] = l_nodes.size() + r_nodes.size() + gid;
}
// add edge from source to left nodes
for (int idx = 0; idx < l_nodes.size(); ++idx) {
int capacity = u_capacity.empty() ? 1 : u_capacity[idx];
add_edge(source_id, idx, capacity);
}
// add edge from right nodes to sink node
// add edge from right nodes to v_node_to(sink node or temp group node)
for (int idx = 0; idx < r_nodes.size(); ++idx) {
int capacity = v_capacity.empty() ? 1 : v_capacity[idx];
add_edge(l_nodes.size() + idx, sink_id, capacity);
add_edge(l_nodes.size() + idx, v_node_to[idx], capacity);
}
// add edge from temp group node to sink node
for (int idx = 0; idx < v_group_capacity.size(); ++idx) {
int capacity = v_group_capacity[idx].second;
add_edge(l_nodes.size() + r_nodes.size() + idx, sink_id, capacity);
}
// add edge from left nodes to right nodes
@@ -269,6 +364,301 @@ namespace Slic3r
return m_solver->solve();
}
// ==================== GeneralMinCostLowerBoundsSolver ====================
GeneralMinCostLowerBoundsSolver::~GeneralMinCostLowerBoundsSolver() = default;
GeneralMinCostLowerBoundsSolver::GeneralMinCostLowerBoundsSolver(const std::vector<FlushMatrix> &matrix_,
const std::vector<int> &u_nodes,
const std::vector<int> &v_nodes,
const std::vector<int> &v_nodes_group,
const std::unordered_map<int, std::vector<int>> &uv_link_limits,
const std::unordered_map<int, std::vector<int>> &uv_unlink_limits)
{
flush_matrix = matrix_;
l_nodes = u_nodes;
r_nodes = v_nodes;
r_nodes_group = v_nodes_group;
m_uv_link_limits = uv_link_limits;
m_uv_unlink_limits = uv_unlink_limits;
num_groups = *std::max_element(r_nodes_group.begin(), r_nodes_group.end()) + 1;
m_solver_lower_bounds = std::make_unique<MaxFlowWithLowerBounds>();
m_solver_min_cost = std::make_unique<MinCostMaxFlow>();
}
std::vector<int> GeneralMinCostLowerBoundsSolver::solve()
{
// group nodes that do not need a lower-bound constraint
std::unordered_set<int> no_lower_group;
for (int i = 0; i < r_nodes.size(); i++) {
if (r_nodes[i] >= 0)
no_lower_group.insert(r_nodes_group[i]);
}
// 1. build the lower-bound network graph
build_feasible_graph(no_lower_group);
// 2. compute the max flow
int need = 0;
for (int d : demand)
if (d > 0) need += d;
std::vector<int> feasible_matching;
int pushed_flow = m_solver_lower_bounds->solve(feasible_matching);
assert(need == pushed_flow);
// 3. convert the lower-bound max-flow network into a min-cost-max-flow network
build_graph_with_feasible_result();
// 4. compute the min-cost max-flow
auto min_cost_matching = m_solver_min_cost->solve();
return min_cost_matching;
}
void GeneralMinCostLowerBoundsSolver::build_feasible_graph(const std::unordered_set<int> &no_lower_groups)
{
m_solver_lower_bounds->l_nodes = l_nodes;
m_solver_lower_bounds->r_nodes = r_nodes;
m_solver_lower_bounds->total_nodes = l_nodes.size() + r_nodes.size() + num_groups + 2;
m_solver_lower_bounds->source_id = m_solver_lower_bounds->total_nodes - 2;
m_solver_lower_bounds->sink_id = m_solver_lower_bounds->total_nodes - 1;
m_solver_lower_bounds->adj.resize(m_solver_lower_bounds->total_nodes);
demand.resize(m_solver_lower_bounds->total_nodes, 0);
const int L = m_solver_lower_bounds->l_nodes.size();
const int R = m_solver_lower_bounds->r_nodes.size();
// source -> l
for (int i = 0; i < L; ++i)
m_solver_lower_bounds->add_edge(m_solver_lower_bounds->source_id, i, 1);
// u -> v (with link/unlink limits)
for (int i = 0; i < L; ++i) {
if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) {
for (int j : it->second)
m_solver_lower_bounds->add_edge(i, L + j, 1);
continue;
}
std::optional<std::vector<int>> unlink_limits;
if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end())
unlink_limits = it->second;
for (int j = 0; j < R; ++j) {
if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end())
continue;
m_solver_lower_bounds->add_edge(i, L + j, 1);
}
}
// r -> group
for (int j = 0; j < R; ++j) {
int g = r_nodes_group[j];
m_solver_lower_bounds->add_edge(L + j, L + R + g, 1);
}
// group -> sink (lower bound = 1)
for (int g = 0; g < num_groups; ++g) {
if (no_lower_groups.count(g))
m_solver_lower_bounds->add_edge(L + R + g, m_solver_lower_bounds->sink_id, R);
else
add_edge_with_lower_bound(L + R + g, m_solver_lower_bounds->sink_id, 1, R, 0);
}
max_flow_edges = m_solver_lower_bounds->edges.size();
// support lower bounds, add super source super sink
super_source = m_solver_lower_bounds->total_nodes++;
super_sink = m_solver_lower_bounds->total_nodes++;
m_solver_lower_bounds->adj.resize(m_solver_lower_bounds->total_nodes);
demand.resize(m_solver_lower_bounds->total_nodes, 0);
for (int i = 0; i < super_source; ++i) {
if (demand[i] > 0) {
m_solver_lower_bounds->add_edge(super_source, i, demand[i]);
} else if (demand[i] < 0) {
m_solver_lower_bounds->add_edge(i, super_sink, -demand[i]);
}
}
m_solver_lower_bounds->add_edge(m_solver_lower_bounds->sink_id, m_solver_lower_bounds->source_id, MaxFlowGraph::INF);
source_id = m_solver_lower_bounds->source_id;
sink_id = m_solver_lower_bounds->sink_id;
m_solver_lower_bounds->source_id = super_source;
m_solver_lower_bounds->sink_id = super_sink;
}
void GeneralMinCostLowerBoundsSolver::build_graph_with_feasible_result()
{
for (auto&lb:lower_bound_edges){
m_solver_lower_bounds->edges[lb.edge_id].flow += lb.lower;
m_solver_lower_bounds->edges[lb.edge_id ^ 1].flow -= lb.lower;
}
m_solver_min_cost->l_nodes = m_solver_lower_bounds->l_nodes;
m_solver_min_cost->r_nodes = m_solver_lower_bounds->r_nodes;
m_solver_min_cost->source_id = source_id;
m_solver_min_cost->sink_id = sink_id;
m_solver_min_cost->total_nodes = sink_id + 1;
m_solver_min_cost->edges = m_solver_lower_bounds->edges;
m_solver_min_cost->edges.erase(m_solver_min_cost->edges.begin() + max_flow_edges, m_solver_min_cost->edges.end());
m_solver_min_cost->adj = m_solver_lower_bounds->adj;
m_solver_min_cost->adj.resize(m_solver_min_cost->total_nodes);
for (auto &node_edges : m_solver_min_cost->adj) {
node_edges.erase(std::remove_if(node_edges.begin(), node_edges.end(), [this](int val) {return val >= this->max_flow_edges;}), node_edges.end());
}
for (auto& e : m_solver_min_cost->edges) {
int L = m_solver_min_cost->l_nodes.size();
int R = m_solver_min_cost->r_nodes.size();
if (e.from < L && e.to >= L && e.to < L + R) {
int idx_in_left = e.from;
int idx_in_right = e.to - L;
int group_id = r_nodes_group[idx_in_right];
if (r_nodes[idx_in_right] == -1) continue;
e.cost = flush_matrix[group_id][l_nodes[idx_in_left]][r_nodes[idx_in_right]];
}
}
}
void GeneralMinCostLowerBoundsSolver::add_edge_with_lower_bound(int from, int to, int lower, int upper, int cost)
{
int eid = m_solver_lower_bounds->edges.size();
m_solver_lower_bounds->add_edge(from, to, upper - lower);
lower_bound_edges.push_back({eid, lower});
demand[from] -= lower;
demand[to] += lower;
}
// ==================== GroupMinCostFlowSolver ====================
GroupMinCostFlowSolver::~GroupMinCostFlowSolver() = default;
GroupMinCostFlowSolver::GroupMinCostFlowSolver(const std::vector<FlushMatrix> &matrix_,
const std::vector<int> &u_nodes,
const std::vector<int> &v_nodes,
const std::vector<int> &v_nodes_group,
const std::unordered_map<int, std::vector<int>> &uv_link_limits,
const std::unordered_map<int, std::vector<int>> &uv_unlink_limits)
{
flush_matrix = matrix_;
l_nodes = u_nodes;
r_nodes = v_nodes;
r_nodes_group = v_nodes_group;
m_uv_link_limits = uv_link_limits;
m_uv_unlink_limits = uv_unlink_limits;
num_groups = *std::max_element(r_nodes_group.begin(), r_nodes_group.end()) + 1;
m_solver = std::make_unique<MinCostMaxFlow>();
build_graph();
}
int GroupMinCostFlowSolver::get_flush_cost(int l_idx, int r_idx)
{
if (r_nodes[r_idx] == -1)
return 0;
int group_id = r_nodes_group[r_idx];
return (int)flush_matrix[group_id][l_nodes[l_idx]][r_nodes[r_idx]];
}
void GroupMinCostFlowSolver::build_graph()
{
const int L = (int)l_nodes.size();
const int R = (int)r_nodes.size();
const int G = num_groups;
m_solver->l_nodes = l_nodes;
m_solver->r_nodes = r_nodes;
m_solver->total_nodes = L + R + G + 2;
m_solver->source_id = L + R + G;
m_solver->sink_id = L + R + G + 1;
m_solver->adj.resize(m_solver->total_nodes);
int max_flush = 0;
for (const auto &mat : flush_matrix)
for (const auto &row : mat)
for (float v : row)
max_flush = std::max(max_flush, (int)v);
int bonus = max_flush * L + 1;
// source -> l_i
for (int i = 0; i < L; ++i)
m_solver->add_edge(m_solver->source_id, i, 1, 0);
// l_i -> r_j (with link/unlink limits)
for (int i = 0; i < L; ++i) {
if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) {
for (int j : it->second)
m_solver->add_edge(i, L + j, 1, get_flush_cost(i, j));
continue;
}
std::optional<std::vector<int>> unlink_limits;
if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end())
unlink_limits = it->second;
for (int j = 0; j < R; ++j) {
if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end())
continue;
m_solver->add_edge(i, L + j, 1, get_flush_cost(i, j));
}
}
// r_j -> group_g
// Compute per-nozzle incoming edge count as capacity upper bound.
// When unlink_limits restrict multiple filaments to the same nozzle,
// capacity=1 would block valid assignments. Using the actual in-degree
// allows the necessary flow while still preserving nozzle-level balance
// (a nozzle with fewer forced filaments keeps a tighter cap).
// The first unit carries a small nozzle-bonus to encourage spreading
// filaments across distinct nozzles within the same group.
int nozzle_bonus = max_flush + 1;
std::vector<int> r_in_degree(R, 0);
for (int i = 0; i < L; ++i) {
if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) {
for (int j : it->second)
r_in_degree[j]++;
continue;
}
std::optional<std::vector<int>> unlink_limits;
if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end())
unlink_limits = it->second;
for (int j = 0; j < R; ++j) {
if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end())
continue;
r_in_degree[j]++;
}
}
for (int j = 0; j < R; ++j) {
int g = r_nodes_group[j];
int cap = std::max(r_in_degree[j], 1);
// First unit gets -nozzle_bonus to prefer using distinct nozzles
m_solver->add_edge(L + j, L + R + g, 1, -nozzle_bonus);
if (cap > 1)
m_solver->add_edge(L + j, L + R + g, cap - 1, 0);
}
// group_g -> sink (split: first unit gets -bonus, rest gets 0)
// bonus >> nozzle_bonus, so group coverage always takes priority
for (int g = 0; g < G; ++g) {
m_solver->add_edge(L + R + g, m_solver->sink_id, 1, -bonus);
if (L > 1)
m_solver->add_edge(L + R + g, m_solver->sink_id, L - 1, 0);
}
}
std::vector<int> GroupMinCostFlowSolver::solve()
{
return m_solver->solve();
}
// ==================== MinFlushFlowSolver ====================
MinFlushFlowSolver::~MinFlushFlowSolver()
{
}
@@ -277,7 +667,8 @@ namespace Slic3r
const std::unordered_map<int, std::vector<int>>& uv_link_limits,
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits,
const std::vector<int>& u_capacity,
const std::vector<int>& v_capacity)
const std::vector<int>& v_capacity,
const std::vector<std::pair<std::set<int>,int>>&v_group_capacity)
{
assert(u_capacity.empty() || u_capacity.size() == u_nodes.size());
assert(v_capacity.empty() || v_capacity.size() == v_nodes.size());
@@ -286,13 +677,19 @@ namespace Slic3r
m_solver->l_nodes = u_nodes;
m_solver->r_nodes = v_nodes;
m_solver->total_nodes = u_nodes.size() + v_nodes.size() + 2;
m_solver->total_nodes = u_nodes.size() + v_nodes.size() + v_group_capacity.size() + 2;
m_solver->source_id =m_solver->total_nodes - 2;
m_solver->sink_id = m_solver->total_nodes - 1;
m_solver->adj.resize(m_solver->total_nodes);
std::vector<int> v_node_to(v_nodes.size(), m_solver->sink_id);
for (size_t gid = 0; gid < v_group_capacity.size(); ++gid) {
for (auto vid : v_group_capacity[gid].first)
v_node_to[vid] = m_solver->l_nodes.size() + m_solver->r_nodes.size() + gid;
}
// add edge from source to left nodes,cost to 0
for (int i = 0; i < m_solver->l_nodes.size(); ++i) {
int capacity = u_capacity.empty() ? 1 : u_capacity[i];
@@ -301,7 +698,12 @@ namespace Slic3r
// add edge from right nodes to sink,cost to 0
for (int i = 0; i < m_solver->r_nodes.size(); ++i) {
int capacity = v_capacity.empty() ? 1 : v_capacity[i];
m_solver->add_edge(m_solver->l_nodes.size() + i, m_solver->sink_id, capacity, 0);
m_solver->add_edge(m_solver->l_nodes.size() + i, v_node_to[i], capacity, 0);
}
// add edge from temp group node to sink node
for(int i=0;i<v_group_capacity.size();++i){
int capacity = v_group_capacity[i].second;
m_solver->add_edge(m_solver->l_nodes.size() + m_solver->r_nodes.size() + i, m_solver->sink_id, capacity, 0);
}
// add edge from left node to right nodes
for (int i = 0; i < m_solver->l_nodes.size(); ++i) {
@@ -602,12 +1004,135 @@ namespace Slic3r
}
// Single-nozzle flush-minimizing reorder over one filament set / one flush matrix, with an
// optional seed filament. Extracted from the group loop so the multi-nozzle reorder can call it
// per physical nozzle.
// TODO: add custom sequence
static int reorder_filaments_for_minimum_flush_volume_base(const std::vector<unsigned int>& filament_lists,
const std::vector<std::vector<unsigned int>>& layer_filaments,
const FlushMatrix& flush_matrix,
const std::function<bool(int, std::vector<int>&)> get_custom_seq,
std::vector<std::vector<unsigned int>>* filament_sequences,
std::optional<unsigned int> initial_filament_id = std::nullopt)
{
constexpr int max_n_with_forcast = 5;
using uint128_t = boost::multiprecision::uint128_t;
if (filament_sequences) {
filament_sequences->clear();
filament_sequences->reserve(layer_filaments.size());
}
auto filament_list_to_hash_key = [](const std::vector<unsigned int>& curr_layer_filaments, const std::vector<unsigned int>& next_layer_filaments,
const std::optional<unsigned int>& prev_filament, bool use_forcast) -> uint128_t {
uint128_t hash_key = 0;
// 31-0 bit define current layer extruder,63-32 bit define next layer extruder,95~64 define prev extruder
if (prev_filament) hash_key |= (uint128_t(1) << (64 + *prev_filament));
if (use_forcast) {
for (auto item : next_layer_filaments) { hash_key |= (uint128_t(1) << (32 + item)); }
}
for (auto item : curr_layer_filaments) { hash_key |= (uint128_t(1) << item); }
return hash_key;
};
int cost = 0;
std::map<size_t, std::vector<unsigned int>> custom_layer_sequence_map;
std::unordered_map<uint128_t, std::pair<float, std::vector<unsigned int>>> caches;
std::unordered_set<unsigned int> filament_sets(filament_lists.begin(), filament_lists.end());
std::optional<unsigned int> curr_filament_id;
// use the provided initial filament id as the starting state when it is valid
if (initial_filament_id.has_value() && *initial_filament_id < flush_matrix.size()) {
curr_filament_id = initial_filament_id;
}
for (size_t layer = 0; layer < layer_filaments.size(); ++layer){
const auto& curr_lf = layer_filaments[layer];
std::vector<int> custom_filament_seq;
if (get_custom_seq && get_custom_seq(layer, custom_filament_seq) && !custom_filament_seq.empty()) {
std::vector<unsigned int> unsign_custom_extruder_seq;
for (int extruder : custom_filament_seq) {
unsigned int unsign_extruder = static_cast<unsigned int>(extruder) - 1;
auto it = std::find(layer_filaments[layer].begin(), layer_filaments[layer].end(), unsign_extruder);
if (it != layer_filaments[layer].end())
unsign_custom_extruder_seq.emplace_back(unsign_extruder);
}
assert(layer_filaments[layer].size() == unsign_custom_extruder_seq.size());
custom_layer_sequence_map[layer] = unsign_custom_extruder_seq;
}
}
for (size_t layer = 0; layer < layer_filaments.size(); ++layer) {
const auto& curr_lf = layer_filaments[layer];
if(auto iter = custom_layer_sequence_map.find(layer); iter != custom_layer_sequence_map.end()){
auto sequence_in_group = collect_filaments_in_groups<unsigned int>(std::unordered_set<unsigned int>(filament_lists.begin(),filament_lists.end()), iter->second);
std::optional<unsigned int> prev = curr_filament_id;
for (auto& f: sequence_in_group){
if(prev)
cost += flush_matrix[*prev][f];
prev = f;
}
if(!sequence_in_group.empty()){
curr_filament_id = sequence_in_group.back();
}
if(filament_sequences)
filament_sequences->emplace_back(sequence_in_group);
continue;
}
std::vector<unsigned int> filament_used = collect_filaments_in_groups<unsigned int>(filament_sets, curr_lf);
std::vector<unsigned int> next_lf;
if (layer + 1 < layer_filaments.size()) next_lf = layer_filaments[layer + 1];
std::vector<unsigned int> filament_used_next_layer = collect_filaments_in_groups<unsigned int>(filament_sets, next_lf);
// Enable inter-layer forecast: when choosing filament ordering for current layer,
// also consider next layer's filament set to minimize inter-layer transition flush.
// solve_extruder_order_with_forcast() tries all permutations of curr+next layer
// and picks the ordering that minimizes total flush across both layers.
// This avoids expensive inter-layer transitions (e.g. ending layer with F2 when
// next layer starts with F3, costing flush[F2→F3], instead of ending with F3
// which gives flush[F3→F3]=0). Limited to ≤5 filaments due to O(N!×M!) complexity.
// The per-nozzle base reorder does not use the inter-layer forecast. This function drives
// BBL multi-extruder grouping cost and H2C ordering, so keeping it false avoids perturbing
// existing H2D/H2C output.
bool use_forcast = false;
float tmp_cost = 0;
std::vector<unsigned int> sequence;
uint128_t hash_key = filament_list_to_hash_key(filament_used, filament_used_next_layer, curr_filament_id, use_forcast);
if (auto iter = caches.find(hash_key); iter != caches.end()) {
tmp_cost = iter->second.first;
sequence = iter->second.second;
}
else {
sequence = get_extruders_order(flush_matrix, filament_used, filament_used_next_layer, curr_filament_id, use_forcast, &tmp_cost);
caches[hash_key] = { tmp_cost,sequence };
}
if (filament_sequences)
filament_sequences->emplace_back(sequence);
if (!sequence.empty())
curr_filament_id = sequence.back();
cost += tmp_cost;
}
return cost;
}
int reorder_filaments_for_minimum_flush_volume(const std::vector<unsigned int>& filament_lists,
const std::vector<int>& filament_maps,
const std::vector<std::vector<unsigned int>>& layer_filaments,
const std::vector<FlushMatrix>& flush_matrix,
std::optional<std::function<bool(int, std::vector<int>&)>> get_custom_seq,
std::vector<std::vector<unsigned int>>* filament_sequences)
std::vector<std::vector<unsigned int>>* filament_sequences,
const std::unordered_map<int, int>& nozzle_status)
{
//only when layer filament num <= 5,we do forcast
constexpr int max_n_with_forcast = 5;
@@ -670,6 +1195,12 @@ namespace Slic3r
if (groups[idx].empty())
continue;
std::optional<unsigned int>current_extruder_id;
// seed the group (nozzle) with the filament already loaded, if nozzle_status supplies one
if (auto it = nozzle_status.find(static_cast<int>(idx)); it != nozzle_status.end() && it->second >= 0) {
unsigned int initial_fil = static_cast<unsigned int>(it->second);
if (initial_fil < flush_matrix[idx].size())
current_extruder_id = initial_fil;
}
std::unordered_map<uint128_t, std::pair<float, std::vector<unsigned int>>> caches;
@@ -775,4 +1306,174 @@ namespace Slic3r
return cost;
}
int reorder_filaments_for_multi_nozzle_extruder(const std::vector<unsigned int>& filament_lists,
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result,
const std::vector<std::vector<unsigned int>>& layer_filaments,
const std::vector<FlushMatrix>& flush_matrix,
const std::function<bool(int, std::vector<int>&)> get_custom_seq,
std::vector<std::vector<unsigned int>>* filament_sequences,
const MultiNozzleUtils::NozzleStatusRecorder& initial_status)
{
std::map<int,std::set<unsigned int>> nozzle_filament_groups;
std::map<int,std::set<int>> extruder_to_nozzle;
for(auto filament_idx : filament_lists){
auto nozzle_info = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1);
if (!nozzle_info)
continue;
nozzle_filament_groups[nozzle_info->group_id].insert(filament_idx);
extruder_to_nozzle[nozzle_info->extruder_id].insert(nozzle_info->group_id);
}
std::map<size_t, std::vector<unsigned int>>custom_layer_sequence_map;// save the filament sequences of custom layer
for (size_t layer = 0; layer < layer_filaments.size(); ++layer){
const auto& curr_lf = layer_filaments[layer];
std::vector<int> custom_filament_seq;
if (get_custom_seq && get_custom_seq(layer, custom_filament_seq) && !custom_filament_seq.empty()) {
std::vector<unsigned int> unsign_custom_extruder_seq;
for (int extruder : custom_filament_seq) {
unsigned int unsign_extruder = static_cast<unsigned int>(extruder) - 1;
auto it = std::find(layer_filaments[layer].begin(), layer_filaments[layer].end(), unsign_extruder);
if (it != layer_filaments[layer].end())
unsign_custom_extruder_seq.emplace_back(unsign_extruder);
}
assert(layer_filaments[layer].size() == unsign_custom_extruder_seq.size());
custom_layer_sequence_map[layer] = unsign_custom_extruder_seq;
}
}
std::map<int, std::vector<std::vector<unsigned int>>> nozzle_filament_sequences;
bool store_sequence = filament_sequences != nullptr;
int cost = 0;
for(auto& group : nozzle_filament_groups){
int nozzle_id = group.first;
auto& filament_in_nozzle = group.second;
int extruder_id = 0;
for(auto& [ext, nozzle_set] : extruder_to_nozzle){
if(nozzle_set.count(nozzle_id)){
extruder_id = ext;
break;
}
}
if(filament_in_nozzle.empty())
continue;
std::vector<unsigned int> filament_vec_in_nozzle(filament_in_nozzle.begin(), filament_in_nozzle.end());
int initial_fil = initial_status.get_filament_in_nozzle(nozzle_id);
std::optional<unsigned int> initial_fil_id = (initial_fil >= 0 && initial_fil < flush_matrix[extruder_id].size())? std::optional<unsigned int>(initial_fil) : std::nullopt;
std::vector<std::vector<unsigned int>> filament_seq;
cost += reorder_filaments_for_minimum_flush_volume_base(filament_vec_in_nozzle, layer_filaments, flush_matrix[extruder_id], get_custom_seq,
store_sequence ? &filament_seq : nullptr, initial_fil_id);
if(store_sequence)
nozzle_filament_sequences.emplace(nozzle_id, std::move(filament_seq));
}
if(!store_sequence)
return cost;
std::vector<int> extruders;
std::map<int, std::vector<int>> nozzles_per_extruder;
for (auto& [extruder_id, nozzle_set] : extruder_to_nozzle) {
extruders.push_back(extruder_id);
nozzles_per_extruder[extruder_id] = std::vector<int>(
nozzle_set.begin(), nozzle_set.end()
);
}
filament_sequences->clear();
filament_sequences->resize(layer_filaments.size());
// No filament in filament_lists resolved to a nozzle in nozzle_group_result
// (e.g. a degenerate input where a layer references a filament index outside the range's
// grouping map). Emit each layer's filaments in their given order so the caller still gets a
// valid per-layer sequence, and skip the cross-nozzle reorder. Guards the unchecked
// max_element(extruders) below, which would dereference end() on an empty range.
if (extruders.empty()) {
for (size_t layer = 0; layer < layer_filaments.size(); ++layer)
(*filament_sequences)[layer] = layer_filaments[layer];
return cost;
}
auto get_extruder_for_filament = [nozzle_group_result](unsigned int filament_idx) {
auto nozzle = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1);
if (!nozzle)
return -1;
return nozzle->extruder_id;
};
auto get_nozzle_idx_for_filament = [nozzles_per_extruder, nozzle_group_result](unsigned int filament_idx)->int {
auto nozzle = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1);
if (!nozzle)
return -1;
return std::find(nozzles_per_extruder.at(nozzle->extruder_id).begin(), nozzles_per_extruder.at(nozzle->extruder_id).end(), nozzle->group_id) - nozzles_per_extruder.at(nozzle->extruder_id).begin();
};
int initial_extruder = initial_status.get_current_extruder_id();
int last_extruder_idx = (initial_extruder >= 0 && initial_extruder < extruders.size())? initial_extruder : 0;
// set size to max extruder_id in case extruder_id is not continuous
std::vector<int> last_nozzle_idx(*std::max_element(extruders.begin(),extruders.end()) + 1,0);
for (int ext_id = 0; ext_id < static_cast<int>(last_nozzle_idx.size()); ext_id++) {
int initial_nozzle = initial_status.get_nozzle_in_extruder(ext_id);
auto ext_nozzles = nozzles_per_extruder[ext_id];
auto it = std::find(ext_nozzles.begin(), ext_nozzles.end(), initial_nozzle);
if (it != ext_nozzles.end())
last_nozzle_idx[ext_id] = static_cast<int>(std::distance(ext_nozzles.begin(), it));
}
for (size_t layer = 0; layer < layer_filaments.size(); ++layer) {
auto& out_seq = (*filament_sequences)[layer];
if (custom_layer_sequence_map.find(layer) != custom_layer_sequence_map.end()) {
out_seq = custom_layer_sequence_map[layer];
if (!out_seq.empty()) {
last_extruder_idx = get_extruder_for_filament(out_seq.back());
for (auto filament : out_seq) {
int cur_ext_id = get_extruder_for_filament(filament);
last_nozzle_idx[cur_ext_id] = get_nozzle_idx_for_filament(filament);
}
}
continue;
}
if (last_extruder_idx == -1)
last_extruder_idx = 0;
int curr_last_extruder_idx = last_extruder_idx;
auto curr_last_nozzle_idx = last_nozzle_idx;
for (int i = 0; i < extruders.size(); ++i) {
int extruder_id = extruders[(last_extruder_idx + i) % extruders.size()];
auto& base_nozzles = nozzles_per_extruder[extruder_id];
bool has_seq = false;
if (last_nozzle_idx[extruder_id] == -1)
last_nozzle_idx[extruder_id] = 0;
for (int j = 0; j < base_nozzles.size(); ++j) {
int nozzle_idx = (last_nozzle_idx[extruder_id] + j) % base_nozzles.size();
int nozzle_id = base_nozzles[nozzle_idx];
const auto& frag = nozzle_filament_sequences[nozzle_id][layer];
if (frag.empty())
continue;
has_seq = true;
curr_last_nozzle_idx[extruder_id] = nozzle_idx;
out_seq.insert(out_seq.end(), frag.begin(), frag.end());
}
if (has_seq)
curr_last_extruder_idx = extruder_id;
}
last_extruder_idx = curr_last_extruder_idx;
last_nozzle_idx = curr_last_nozzle_idx;
}
return cost;
}
}
+109 -8
View File
@@ -6,7 +6,10 @@
#include <functional>
#include <limits>
#include <memory>
#include <set>
#include <unordered_set>
#include <unordered_map>
#include "../MultiNozzleUtils.hpp"
namespace Slic3r {
@@ -15,21 +18,27 @@ using FlushMatrix = std::vector<std::vector<float>>;
namespace MaxFlowGraph {
const int INF = std::numeric_limits<int>::max();
const int INVALID_ID = -1;
// Upper bound for MCMF edge cost to prevent int overflow in SPFA causing infinite loops
constexpr int MCMF_MAX_EDGE_COST = 10000000;
}
// Namespace-scope edge shared by the max-flow / min-cost-max-flow solvers below.
// The default cost keeps the plain max-flow solvers (which never read cost) source-compatible.
struct Edge
{
int from, to, capacity, cost, flow;
Edge(int u, int v, int cap, int cst = 0) : from(u), to(v), capacity(cap), cost(cst), flow(0) {}
};
class MaxFlowSolver
{
private:
struct Edge {
int from, to, capacity, flow;
Edge(int u, int v, int cap) :from(u), to(v), capacity(cap), flow(0) {}
};
public:
MaxFlowSolver(const std::vector<int>& u_nodes, const std::vector<int>& v_nodes,
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {},
const std::vector<int>& u_capacity = {},
const std::vector<int>& v_capacity = {}
const std::vector<int>& v_capacity = {},
const std::vector<std::pair<std::set<int>, int>>& v_group_capacity = {}
);
std::vector<int> solve();
@@ -47,6 +56,7 @@ private:
struct MinCostMaxFlow;
struct MaxFlowWithLowerBounds;
class GeneralMinCostSolver
{
@@ -61,6 +71,84 @@ private:
std::unique_ptr<MinCostMaxFlow> m_solver;
};
class GeneralMinCostLowerBoundsSolver
{
public:
GeneralMinCostLowerBoundsSolver(
const std::vector<FlushMatrix> &matrix_,
const std::vector<int>& u_nodes,
const std::vector<int>& v_nodes,
const std::vector<int>& v_nodes_group,
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {});
std::vector<int> solve();
~GeneralMinCostLowerBoundsSolver();
private:
void build_feasible_graph(const std::unordered_set<int>& no_lower_groups);
void build_graph_with_feasible_result();
void add_edge_with_lower_bound(int from, int to, int lower, int upper, int cost);
int get_distance(const int idx_in_left,const int idx_in_right);
private:
std::unique_ptr<MaxFlowWithLowerBounds> m_solver_lower_bounds;
std::unique_ptr<MinCostMaxFlow> m_solver_min_cost;
std::vector<FlushMatrix> flush_matrix;
std::vector<int> l_nodes;
std::vector<int> r_nodes;
std::vector<int> r_nodes_group;
std::unordered_map<int, std::vector<int>> m_uv_link_limits;
std::unordered_map<int, std::vector<int>> m_uv_unlink_limits;
int num_groups = 0;
// support lower bounds
struct LowerBoundEdge{
int edge_id;
int lower;
};
std::vector<int> demand;
std::vector<LowerBoundEdge> lower_bound_edges;
int super_source = -1;
int super_sink = -1;
int source_id = -1;
int sink_id = -1;
int max_flow_edges = 0;
};
class GroupMinCostFlowSolver
{
public:
GroupMinCostFlowSolver(
const std::vector<FlushMatrix> &matrix_,
const std::vector<int> &u_nodes,
const std::vector<int> &v_nodes,
const std::vector<int> &v_nodes_group,
const std::unordered_map<int, std::vector<int>> &uv_link_limits = {},
const std::unordered_map<int, std::vector<int>> &uv_unlink_limits = {});
std::vector<int> solve();
~GroupMinCostFlowSolver();
private:
void build_graph();
int get_flush_cost(int l_idx, int r_idx);
std::unique_ptr<MinCostMaxFlow> m_solver;
std::vector<FlushMatrix> flush_matrix;
std::vector<int> l_nodes;
std::vector<int> r_nodes;
std::vector<int> r_nodes_group;
std::unordered_map<int, std::vector<int>> m_uv_link_limits;
std::unordered_map<int, std::vector<int>> m_uv_unlink_limits;
int num_groups = 0;
};
class MinFlushFlowSolver
{
@@ -71,7 +159,8 @@ public:
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {},
const std::vector<int>& u_capacity = {},
const std::vector<int>& v_capacity = {}
const std::vector<int>& v_capacity = {},
const std::vector<std::pair<std::set<int>, int>>& v_group_capacity = {}
);
std::vector<int> solve();
~MinFlushFlowSolver();
@@ -108,7 +197,19 @@ int reorder_filaments_for_minimum_flush_volume(const std::vector<unsigned int> &
const std::vector<std::vector<unsigned int>> &layer_filaments,
const std::vector<FlushMatrix> &flush_matrix,
std::optional<std::function<bool(int, std::vector<int> &)>> get_custom_seq,
std::vector<std::vector<unsigned int>> *filament_sequences);
std::vector<std::vector<unsigned int>> *filament_sequences,
const std::unordered_map<int, int>& nozzle_status = {});
// Order filaments within a per-nozzle grouping result (multi-nozzle extruders). Threads a
// NozzleStatusRecorder describing the initial physical nozzle occupancy so the reorder can reward
// keeping an already-loaded filament in place.
int reorder_filaments_for_multi_nozzle_extruder(const std::vector<unsigned int>& filament_lists,
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result,
const std::vector<std::vector<unsigned int>>& layer_filaments,
const std::vector<FlushMatrix>& flush_matrix,
const std::function<bool(int,std::vector<int>&)> get_custom_seq,
std::vector<std::vector<unsigned int>> * filament_sequences,
const MultiNozzleUtils::NozzleStatusRecorder& initial_status = {});
}
#endif // !TOOL_ORDER_UTILS_HPP
File diff suppressed because it is too large Load Diff
+49 -5
View File
@@ -9,6 +9,7 @@
#include <boost/container/small_vector.hpp>
#include "../FilamentGroup.hpp"
#include "../MultiNozzleUtils.hpp"
#include "../ExtrusionEntity.hpp"
#include "../PrintConfig.hpp"
@@ -98,19 +99,23 @@ private:
struct FilamentChangeStats
{
int filament_flush_weight{0};
// flush_filament_change_count counts filament changes that actually flush a physical nozzle.
// It replaces the former (dead, never populated) extruder_change_count. For single-nozzle-per-
// extruder printers it equals the per-extruder filament_change_count, so GUI stat displays are
// unchanged.
int flush_filament_change_count{0};
int filament_change_count{0};
int extruder_change_count{0};
void clear(){
filament_flush_weight = 0;
filament_change_count = 0;
extruder_change_count = 0;
flush_filament_change_count = 0;
}
FilamentChangeStats& operator+=(const FilamentChangeStats& other) {
this->filament_flush_weight += other.filament_flush_weight;
this->filament_change_count += other.filament_change_count;
this->extruder_change_count += other.extruder_change_count;
this->flush_filament_change_count += other.flush_filament_change_count;
return *this;
}
@@ -118,7 +123,7 @@ struct FilamentChangeStats
FilamentChangeStats ret;
ret.filament_flush_weight = this->filament_flush_weight + other.filament_flush_weight;
ret.filament_change_count = this->filament_change_count + other.filament_change_count;
ret.extruder_change_count = this->extruder_change_count + other.extruder_change_count;
ret.flush_filament_change_count = this->flush_filament_change_count + other.flush_filament_change_count;
return ret;
}
@@ -236,12 +241,44 @@ public:
bool has_wipe_tower() const { return ! m_layer_tools.empty() && m_first_printing_extruder != (unsigned int)-1 && m_layer_tools.front().has_wipe_tower; }
int get_most_used_extruder() const { return most_used_extruder; }
// Logical (extruder, nozzle) grouping of the used filaments, built during reorder.
// For single-nozzle printers this is one logical nozzle per extruder (nozzle id == extruder id).
// Consumed by GCode (get_nozzle_id / get_first_nozzle_for_filament).
const MultiNozzleUtils::LayeredNozzleGroupResult &get_layered_nozzle_group_result() const { return m_nozzle_group_result; }
// Physical nozzle occupancy threading for the sequential (by-object) selector regroup: the
// setter seeds both the initial recorder (the state the per-layer plan starts from) and the
// running recorder (read back after sort_and_build_data via get_nozzle_status()), so each
// object's plan continues from the nozzle state the previous object ended with.
const MultiNozzleUtils::NozzleStatusRecorder &get_nozzle_status() const { return m_nozzle_status; }
void set_nozzle_status(const MultiNozzleUtils::NozzleStatusRecorder &status) { m_initial_nozzle_status = status; m_nozzle_status = status; }
/*
* called in single extruder mode, the value in map are all 0
* called in dual extruder mode, the value in map will be 0 or 1
* 0 based group id
*/
static std::vector<int> get_recommended_filament_maps(const std::vector<std::vector<unsigned int>>& layer_filaments, const Print* print,const FilamentMapMode mode, const std::vector<std::set<int>>& physical_unprintables, const std::vector<std::set<int>>& geometric_unprintables);
// Nozzle-centric grouping. Returns a nozzle-aware LayeredNozzleGroupResult instead of a plain
// extruder-level std::vector<int>. Callers derive the 0/1-based extruder map via
// result.get_extruder_map(). unprintable_volumes / nozzle_status default empty for the static
// path; the per-layer engine supplies non-empty values.
static MultiNozzleUtils::LayeredNozzleGroupResult get_recommended_filament_maps(const std::vector<std::vector<unsigned int>>& layer_filaments, const Print* print,const FilamentMapMode mode, const std::vector<std::set<int>>& physical_unprintables, const std::vector<std::set<int>>& geometric_unprintables, const std::map<int, std::set<NozzleVolumeType>>& unprintable_volumes = {}, const std::unordered_map<int, int>& nozzle_status = {});
// Wrap stitched per-layer filament->nozzle maps from a sequential (by-object) selector regroup
// into one print-wide result. nozzle_map_per_layer / layer_filaments / layer_sequences are the
// per-object planned layers concatenated in print order; nozzle_map_per_layer is taken by value
// and normalized in place. The nozzle list is rebuilt from the print's grouping context. Returns
// an empty result when the wrap fails. Lives here (not in Print) to reach the file-local
// grouping-context builder.
static MultiNozzleUtils::LayeredNozzleGroupResult build_sequential_group_result(
Print* print,
std::vector<std::vector<int>> nozzle_map_per_layer,
const std::vector<std::vector<unsigned int>>& layer_filaments,
const std::vector<std::vector<unsigned int>>& layer_sequences,
const std::vector<unsigned int>& used_filaments,
const std::vector<std::set<int>>& physical_unprintables,
const std::vector<std::set<int>>& geometric_unprintables,
const std::map<int, std::set<NozzleVolumeType>>& unprintable_volumes);
// should be called after doing reorder
FilamentChangeStats get_filament_change_stats(FilamentChangeMode mode);
@@ -283,6 +320,13 @@ private:
FilamentChangeStats m_stats_by_single_extruder;
FilamentChangeStats m_stats_by_multi_extruder_curr;
FilamentChangeStats m_stats_by_multi_extruder_best;
MultiNozzleUtils::LayeredNozzleGroupResult m_nozzle_group_result;
// Physical nozzle occupancy threaded through the per-layer selector regroup.
// m_initial_nozzle_status seeds the first combo range (empty for a fresh slice — there is no
// device continuation state); m_nozzle_status carries the running state out of the plan. Inert
// for every printer except an H2C profile that enables the filament selector (is_dynamic_group_reorder).
MultiNozzleUtils::NozzleStatusRecorder m_initial_nozzle_status;
MultiNozzleUtils::NozzleStatusRecorder m_nozzle_status;
int most_used_extruder;
};
File diff suppressed because it is too large Load Diff
+125 -61
View File
@@ -12,7 +12,7 @@
#include "libslic3r/Polyline.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <unordered_set>
#include "libslic3r/MultiNozzleUtils.hpp"
namespace Slic3r
{
@@ -20,6 +20,17 @@ class WipeTowerWriter;
class PrintConfig;
enum GCodeFlavor : unsigned char;
// Cuts the tower wall polygon open at each skip point (a toolchange's entry position)
// so the entry travel can pass through instead of crossing the printed wall. Defined in
// WipeTower.cpp, shared by WipeTower and WipeTower2.
Polylines construct_gap_for_skip_points(
const Polygon& polygon, const std::vector<Vec2f>& skip_points, float wt_width, float gap_length, Polygon& insert_skip_polygon);
// Klipper acts on commands the instant it parses them, and its G4 reads only P (milliseconds),
// so the zero-second and seconds-valued dwells every other flavor uses neither synchronize nor
// pause there. Both defined in WipeTower.cpp, shared by WipeTower and WipeTower2.
const char* flush_planner_queue_command(GCodeFlavor flavor); // finish queued moves, e.g. around M104/M109
std::string wait_command(GCodeFlavor flavor, float seconds); // pause for `seconds`
class WipeTower
{
@@ -34,7 +45,11 @@ public:
static TriangleMesh its_make_rib_tower(float width, float depth, float height, float rib_length, float rib_width, bool fillet_wall);
static TriangleMesh its_make_rib_brim(const Polygon& brim, float layer_height);
static Polygon rib_section(float width, float depth, float rib_length, float rib_width, bool fillet_wall);
static Vec2f move_box_inside_box(const BoundingBox &box1, const BoundingBox &box2, int offset = 0);
// Translation that brings a footprint inside the printable outline, padded by offset. The prime
// tower is validated against the real outline (see layered_print_cleareance_valid), so clamping
// against the bounding box alone would leave it off a delta or hexagonal bed. box and polygons
// must share one scaled coordinate frame; the translation comes back in millimeters.
static Vec2f move_box_inside_polygon(const BoundingBox &box, const Polygons &polygons, coord_t offset = 0);
static Polygon rounding_polygon(Polygon &polygon, double rounding = 2., double angle_tol = 30. / 180. * PI);
struct Extrusion
{
@@ -58,6 +73,7 @@ public:
Vec2f origin_start_pos; // not rotated
std::vector<Vec2f> wipe_path;
bool is_extruder_change{true};
};
struct ToolChangeResult
@@ -83,7 +99,6 @@ public:
bool priming;
bool is_tool_change{false};
bool is_contact{false};
Vec2f tool_change_start_pos;
// Pass a polyline so that normal G-code generator can do a wipe for us.
@@ -107,6 +122,7 @@ public:
// executing the gcode finish_layer_tcr.
bool is_finish_first = false;
bool is_contact = false;
NozzleChangeResult nozzle_change_result;
// Sum the total length of the extrusion.
@@ -121,6 +137,8 @@ public:
}
return e_length;
}
// Orca: set by WipeTower2 (non-BBL tower) to force a travel to the tower even when the
// previous position is unknown; read by WipeTowerIntegration::append_tcr2 (GCode.cpp).
bool force_travel = false;
};
@@ -161,15 +179,12 @@ public:
bool priming,
size_t old_tool,
bool is_finish,
bool is_tool_change,
float purge_volume,
bool is_contact = false) const;
bool is_tool_change, float purge_volume, bool is_contact) const;
ToolChangeResult construct_block_tcr(WipeTowerWriter& writer,
bool priming,
size_t filament_id,
bool is_finish,
float purge_volume) const;
bool is_finish, float purge_volume) const;
// x -- x coordinates of wipe tower in mm ( left bottom corner )
@@ -183,9 +198,14 @@ public:
// Set the extruder properties.
void set_extruder(size_t idx, const PrintConfig& config);
void set_shared_print_bed(const Polygons &bed) { m_shared_print_bed = bed; }
// Orca: has_filament_switcher is not a static PrintConfig member here, so it is pushed in from
// Print via a setter rather than read in the ctor. Device-set only.
void set_has_filament_switcher(bool v) { m_has_filament_switcher = v; }
// Appends into internal structure m_plan containing info about the future wipe tower
// to be used before building begins. The entries must be added ordered in z.
void plan_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, float wipe_volume = 0.f, float prime_volume = 0.f);
void plan_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, float wipe_volume_ec = 0.f, float wipe_volume_nc = 0.f, float prime_volume = 0.f);
// Iterates through prepared m_plan, generates ToolChangeResults and appends them to "result"
void generate(std::vector<std::vector<ToolChangeResult>> &result);
@@ -218,9 +238,6 @@ public:
}
}
void set_wipe_volume(std::vector<std::vector<float>>& wiping_matrix) {
wipe_volumes = wiping_matrix;
}
// Switch to a next layer.
void set_layer(
@@ -249,7 +266,6 @@ public:
// Calculate extrusion flow from desired line width, nozzle diameter, filament diameter and layer_height:
m_extrusion_flow = extrusion_flow(layer_height);
// Advance m_layer_info iterator, making sure we got it right
while (!m_plan.empty() && m_layer_info->z < print_z - WT_EPSILON && m_layer_info+1 != m_plan.end())
++m_layer_info;
@@ -308,11 +324,9 @@ public:
std::vector<float> get_used_filament() const { return m_used_filament_length; }
int get_number_of_toolchanges() const { return m_num_tool_changes; }
void set_filament_map(const std::vector<int> &filament_map) { m_filament_map = filament_map; }
void set_has_tpu_filament(bool has_tpu) { m_has_tpu_filament = has_tpu; }
bool has_tpu_filament() const { return m_has_tpu_filament; }
bool has_tpu_filament() const { return m_has_tpu_filament; }
struct FilamentParameters {
std::string material = "PLA";
int category;
@@ -321,15 +335,15 @@ public:
bool is_support = false;
int nozzle_temperature = 0;
int nozzle_temperature_initial_layer = 0;
int interface_print_temperature = 0;
float loading_speed = 0.f;
float loading_speed_start = 0.f;
float unloading_speed = 0.f;
float unloading_speed_start = 0.f;
float delay = 0.f ;
int cooling_moves = 0;
float cooling_initial_speed = 0.f;
float cooling_final_speed = 0.f;
// BBS: remove useless config
//float loading_speed = 0.f;
//float loading_speed_start = 0.f;
//float unloading_speed = 0.f;
//float unloading_speed_start = 0.f;
//float delay = 0.f ;
//int cooling_moves = 0;
//float cooling_initial_speed = 0.f;
//float cooling_final_speed = 0.f;
float ramming_line_width_multiplicator = 1.f;
float ramming_step_multiplicator = 1.f;
float max_e_speed = std::numeric_limits<float>::max();
@@ -339,29 +353,41 @@ public:
float retract_length;
float retract_speed;
float wipe_dist;
float tower_interface_pre_extrusion_dist = 0.f;
float tower_interface_pre_extrusion_length = 0.f;
float tower_ironing_area = 4.f;
float tower_interface_purge_length = 0.f;
std::pair<float,float> max_e_ramming_speed;//[0]extruder change [1]nozzle change
std::pair<float, float> ramming_travel_time; // Travel time after ramming
std::pair<std::vector<float>,std::vector<float>> precool_t;//Pre-cooling time, set to 0 to ensure the ramming speed is controlled solely by ramming volumetric speed.
std::pair<std::vector<float>, std::vector<float>> precool_t_first_layer;
std::pair<int,int> precool_target_temp;
float filament_cooling_before_tower = 0.f;
float flat_iron_area;
float filament_tower_interface_print_temp;
float filament_tower_interface_pre_extrusion_dist = 0;
float filament_tower_interface_pre_extrusion_length = 0;
float filament_petg_pre_extrusion_offset_dist = 0;
};
void set_used_filament_ids(const std::vector<int> &used_filament_ids) { m_used_filament_ids = used_filament_ids; };
void set_used_filament_ids(const std::vector<int> &used_filament_ids) { m_used_filament_ids = used_filament_ids; };
void set_filament_categories(const std::vector<int> & filament_categories) { m_filament_categories = filament_categories;};
std::vector<int> m_used_filament_ids;
void set_nozzle_group_result(const MultiNozzleUtils::LayeredNozzleGroupResult &multi_nozzle_group_result) { m_multi_nozzle_group_result = &multi_nozzle_group_result; };
std::vector<int> m_used_filament_ids;
std::vector<int> m_filament_categories;
const MultiNozzleUtils::LayeredNozzleGroupResult *m_multi_nozzle_group_result{nullptr};
enum class WipeTowerLayerType : unsigned char { Normal, Contact, Solid, Contact_UP};// Contact layer should be solid and reduce feed
struct WipeTowerBlock
{
int block_id{0};
int filament_adhesiveness_category{0};
std::vector<float> layer_depths;
std::vector<bool> solid_infill;
//std::vector<bool> solid_infill;
std::vector<float> finish_depth{0}; // the start pos of finish frame for every layer
std::vector<WipeTowerLayerType> layers_type; // type of the layer, normal, Contact or Solid
float depth{0};
float start_depth{0};
float cur_depth{0};
int last_filament_change_id{-1};
int last_filament_change_id{-1};
int last_nozzle_change_id{-1};
};
@@ -381,23 +407,33 @@ public:
WipeTowerBlock* get_block_by_category(int filament_adhesiveness_category, bool create);
void add_depth_to_block(int filament_id, int filament_adhesiveness_category, float depth, bool is_nozzle_change = false);
int get_filament_category(int filament_id);
bool is_in_same_extruder(int filament_id_1, int filament_id_2);
void reset_block_status();
int get_wall_filament_for_all_layer();
// for generate new wipe tower
void generate_new(std::vector<std::vector<WipeTower::ToolChangeResult>> &result);
void plan_tower_new();
void generate_wipe_tower_blocks();
void generate_wipe_tower_blocks(bool add_solid_flag);
void update_all_layer_depth(float wipe_tower_depth);
void set_nozzle_last_layer_id();
void set_first_layer_flow_ratio(const float flow_ratio);
// Orca: default/initial-layer/travel acceleration are object-scope options here (PrintConfig
// members in BBS), so Print pushes the resolved per-variant columns in via this setter.
void set_accelerations(const std::vector<double> &normal, const std::vector<double> &first_layer_normal,
const std::vector<double> &travel, const std::vector<double> &first_layer_travel);
void calc_block_infill_gap();
ToolChangeResult tool_change_new(size_t new_tool, bool solid_change = false, bool solid_nozzlechange=false);
NozzleChangeResult nozzle_change_new(int old_filament_id, int new_filament_id, bool solid_change = false);
NozzleChangeResult ramming(int old_filament_id, int new_filament_id, bool solid_change = false, bool extruder_change = true); // extruder_chang means nozzle_change
ToolChangeResult finish_layer_new(bool extrude_perimeter = true, bool extrude_fill = true, bool extrude_fill_wall = true);
ToolChangeResult finish_block(const WipeTowerBlock &block, int filament_id, bool extrude_fill = true);
ToolChangeResult finish_block_solid(const WipeTowerBlock &block, int filament_id, bool extrude_fill = true ,bool interface_solid =false);
ToolChangeResult finish_block_solid(const WipeTowerBlock &block, int filament_id, bool extrude_fill = true, WipeTowerLayerType layer_type = WipeTowerLayerType::Normal);
void toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinates &cleaning_box, float wipe_length,bool solid_toolchange=false);
Vec2f get_rib_offset() const { return m_rib_offset; }
bool is_need_ramming(int filament_id_1, int filament_id_2, int layer_id) const;
bool is_same_extruder(int filament_id_1, int filament_id_2, int layer_id) const;
bool is_same_nozzle(int filament_id_1, int filament_id_2, int layer_id) const;
int get_nozzle_id(int filament_id, int layer_id) const;
int get_extruder_id(int filament_id, int layer_id) const;
private:
enum wipe_shape // A fill-in direction
@@ -417,7 +453,6 @@ private:
bool m_enable_wrapping_detection = false;
bool m_enable_timelapse_print = false;
bool m_semm = true; // Are we using a single extruder multimaterial printer?
bool m_purge_in_prime_tower = false; // Do we purge in the prime tower?
Vec2f m_wipe_tower_pos; // Left front corner of the wipe tower in mm.
float m_wipe_tower_width; // Width of the wipe tower.
float m_wipe_tower_depth = 0.f; // Depth of the wipe tower
@@ -435,11 +470,11 @@ private:
float m_travel_speed = 0.f;
float m_first_layer_speed = 0.f;
size_t m_first_layer_idx = size_t(-1);
std::vector<double> m_filaments_change_length;
Vec2f m_origin;
std::vector<int> m_last_layer_id;
std::pair<std::vector<double>,std::vector<double>> m_filaments_change_length;//[0]extruder change [1]nozzle change
size_t m_cur_layer_id;
NozzleChangeResult m_nozzle_change_result;
std::vector<int> m_filament_map;
bool m_has_tpu_filament{false};
bool m_is_multi_extruder{false};
bool m_use_gap_wall{false};
@@ -450,17 +485,32 @@ private:
bool m_used_fillet{false};
Vec2f m_rib_offset{Vec2f(0.f, 0.f)};
bool m_tower_framework{false};
bool m_need_reverse_travel{false};
bool m_enable_tower_interface_features{false};
// G-code generator parameters.
float m_cooling_tube_retraction = 0.f;
float m_cooling_tube_length = 0.f;
float m_parking_pos_retraction = 0.f;
float m_extra_loading_move = 0.f;
// BBS: remove useless config
//float m_cooling_tube_retraction = 0.f;
//float m_cooling_tube_length = 0.f;
//float m_parking_pos_retraction = 0.f;
//float m_extra_loading_move = 0.f;
float m_bridging = 0.f;
bool m_no_sparse_layers = false;
bool m_set_extruder_trimpot = false;
// BBS: remove useless config
//bool m_set_extruder_trimpot = false;
bool m_adhesion = true;
GCodeFlavor m_gcode_flavor;
bool m_is_multiple_nozzle = false;
std::vector<unsigned int> m_normal_accels;
std::vector<unsigned int> m_first_layer_normal_accels;
std::vector<unsigned int> m_travel_accels;
std::vector<unsigned int> m_first_layer_travel_accels;
unsigned int m_max_accels;
bool m_accel_to_decel_enable;
float m_accel_to_decel_factor;
bool m_enable_arc_fitting = true;
std::vector<double> m_hotend_heating_rate;
std::vector<double> m_hotend_cooling_rate;
Polygons m_shared_print_bed;
// Bed properties
enum {
@@ -471,10 +521,11 @@ private:
float m_bed_width; // width of the bed bounding box
Vec2f m_bed_bottom_left; // bottom-left corner coordinates (for rectangular beds)
float m_first_layer_flow_ratio;
float m_perimeter_width = 0.4f * Width_To_Nozzle_Ratio; // Width of an extrusion line, also a perimeter spacing for 100% infill.
float m_nozzle_change_perimeter_width = 0.4f * Width_To_Nozzle_Ratio;
float m_extrusion_flow = 0.038f; //0.029f;// Extrusion flow is derived from m_perimeter_width, layer height and filament diameter.
std::unordered_map<int, std::pair<float,float>> m_block_infill_gap_width; // categories to infill_gap: toolchange gap, nozzlechange gap
// Extruder specific parameters.
std::vector<FilamentParameters> m_filpar;
@@ -487,43 +538,52 @@ private:
// A fill-in direction (positive Y, negative Y) alternates with each layer.
wipe_shape m_current_shape = SHAPE_NORMAL;
size_t m_current_tool = 0;
// Orca: support mmu wipe tower
std::vector<std::vector<float>> wipe_volumes;
// BBS
//const std::vector<std::vector<float>> wipe_volumes;
float m_depth_traversed = 0.f; // Current y position at the wipe tower.
bool m_current_layer_finished = false;
bool m_left_to_right = true;
float m_extra_spacing = 1.f;
float m_tpu_fixed_spacing = 2;
std::vector<Vec2f> m_wall_skip_points;
float m_max_speed = 5400.f; // the maximum printing speed on the prime tower.
std::vector<std::vector<Vec2f>> m_wall_skip_points;
std::map<float,Polylines> m_outer_wall;
std::vector<double> m_printable_height;
bool is_first_layer() const { return size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; }
bool is_valid_last_layer(int tool, int layer_id, double layer_z) const;
bool m_flat_ironing=false;
bool m_enable_tower_interface_features=false;
bool m_enable_tower_interface_cooldown_during_tower=false;
bool m_prev_layer_had_interface=false;
bool m_current_layer_has_interface=false;
bool m_contact_ironing = false;
bool m_has_filament_switcher = false;
float m_contact_speed = 20 * 60.f;
std::vector<int> m_physical_extruder_map;
// Calculates length of extrusion line to extrude given volume
float volume_to_length(float volume, float line_width, float layer_height) const {
return std::max(0.f, volume / (layer_height * (line_width - layer_height * (1.f - float(M_PI) / 4.f))));
}
// Calculates volume of extrusion line
float length_to_volume(float length,float line_width, float layer_height) const
{
return std::max(0.f, length * (layer_height * (line_width - layer_height * (1.f - float(M_PI) / 4.f))));
}
// Calculates depth for all layers and propagates them downwards
void plan_tower();
// Goes through m_plan and recalculates depths and width of the WT to make it exactly square - experimental
void make_wipe_tower_square();
Vec2f get_next_pos(const WipeTower::box_coordinates &cleaning_box, float wipe_length, bool interface_layer, size_t interface_tool);
Vec2f get_next_pos(const WipeTower::box_coordinates &cleaning_box, float wipe_length, bool solid_toolchange);
// Goes through m_plan, calculates border and finish_layer extrusions and subtracts them from last wipe
void save_on_last_wipe();
bool is_tpu_filament(int filament_id) const;
bool is_petg_filament(int filament_id) const;
bool is_need_reverse_travel(int filament, bool extruder_change) const;
// BBS
box_coordinates align_perimeter(const box_coordinates& perimeter_box);
void set_for_wipe_tower_writer(WipeTowerWriter &writer);
// to store information about tool changes for a given layer
struct WipeTowerInfo{
@@ -536,6 +596,7 @@ private:
float wipe_volume;
float wipe_length;
float nozzle_change_depth{0};
float nozzle_change_length{0};
// BBS
float purge_volume;
ToolChange(size_t old, size_t newtool, float depth=0.f, float ramming_depth=0.f, float fwl=0.f, float wv=0.f, float wl = 0, float pv = 0)
@@ -565,7 +626,7 @@ private:
// ot -1 if there is no such toolchange.
int first_toolchange_to_nonsoluble_nonsupport(
const std::vector<WipeTowerInfo::ToolChange>& tool_changes) const;
WipeTowerInfo::ToolChange set_toolchange(int old_tool, int new_tool, float layer_height, float wipe_volume, float purge_volume,int layer_id);
void toolchange_Unload(
WipeTowerWriter &writer,
const box_coordinates &cleaning_box,
@@ -585,7 +646,10 @@ private:
WipeTowerWriter &writer,
const box_coordinates &cleaning_box,
float wipe_volume);
void get_wall_skip_points(const WipeTowerInfo &layer);
void get_wall_skip_points(const WipeTowerInfo &layer,int layer_id);
void get_all_wall_skip_points();
ToolChangeResult merge_tcr(ToolChangeResult &first, ToolChangeResult &second);
float get_block_gap_width(int tool, bool is_nozzlechangle = false);
};
File diff suppressed because it is too large Load Diff
+77 -12
View File
@@ -17,13 +17,20 @@ namespace Slic3r
class WipeTowerWriter2;
class PrintRegionConfig;
class ConfigBase;
class WipeTower2
{
public:
static const std::string never_skip_tag() { return "_GCODE_WIPE_TOWER_NEVER_SKIP_TAG"; }
// Marks the wait-for-temp-on-wipe-tower M109 so the interface-temp deduplication pass
// in WipeTowerIntegration::append_tcr2 does not strip it.
static const std::string wait_for_temp_tag() { return ";_WAIT_FOR_TEMP_ON_WIPE_TOWER"; }
static std::pair<double, double> get_wipe_tower_cone_base(double width, double height, double depth, double angle_deg);
static std::vector<std::vector<float>> extract_wipe_volumes(const PrintConfig& config);
static std::vector<std::vector<float>> extract_wipe_volumes(const ConfigBase& config);
// Estimated total flush volume of a SEMM print with the given number of filaments,
// used to reserve wipe tower space before the tower is generated.
static float estimate_semm_flush_volume(const ConfigBase& config, size_t filaments_cnt);
// Construct ToolChangeResult from current state of WipeTower2 and WipeTowerWriter2.
@@ -34,6 +41,15 @@ public:
bool is_finish,
bool is_contact = false) const;
// Whether this print cuts wall openings ("skip points") at the toolchange entries.
// Shared with the entry routing in GCode.cpp so the router and the tower agree.
static bool use_gap_wall(const PrintConfig& config);
// Whether the blocking toolchange temperature wait moves onto the wipe tower.
// Shared with the defer flag in GCode.cpp append_tcr2 so the deferral and the
// tower's tagged M109 can never disagree.
static bool wait_for_temp_enabled(const PrintConfig& config);
// x -- x coordinates of wipe tower in mm ( left bottom corner )
// y -- y coordinates of wipe tower in mm ( left bottom corner )
// width -- width of wipe tower in mm ( default 60 mm - leave as it is )
@@ -69,9 +85,9 @@ public:
const float brim = m_wipe_tower_brim_width_real;
return BoundingBoxf(Vec2d(-brim, -brim), Vec2d(double(m_wipe_tower_width) + brim, double(m_wipe_tower_depth) + brim));
}
// WT2 doesn't currently compute a rib-origin compensation like WipeTower (m_rib_offset),
// so expose a zero offset for consistency purposes (to maintain API parity).
Vec2f get_rib_offset() const { return Vec2f::Zero(); }
// Tower-local shift that puts the rib wall's first-layer min corner at the configured
// tower position, like WipeTower::get_rib_offset(). Zero unless the rib wall is used.
Vec2f get_rib_offset() const { return m_rib_offset; }
float get_rib_width() const { return m_rib_width; }
float get_rib_length() const { return m_rib_length; }
@@ -149,6 +165,7 @@ public:
struct FilamentParameters {
std::string material = "PLA";
bool is_soluble = false;
bool is_support = false;
int temperature = 0;
int first_layer_temperature = 0;
int interface_print_temperature = 0;
@@ -220,9 +237,9 @@ private:
float m_perimeter_speed = 0.f;
float m_first_layer_speed = 0.f;
size_t m_first_layer_idx = size_t(-1);
bool m_flat_ironing = false;
bool m_enable_tower_interface_features = false;
bool m_enable_tower_interface_cooldown_during_tower = false;
bool m_wait_for_temp_on_wipe_tower = false;
bool m_prev_layer_had_interface = false;
bool m_current_layer_has_interface = false;
@@ -231,6 +248,12 @@ private:
float m_rib_width = 10;
float m_extra_rib_length = 0;
float m_rib_length = 0;
Vec2f m_rib_offset = Vec2f::Zero();
bool m_use_gap_wall = false;
// Per plan layer, each toolchange's entry position (tower-local, un-shifted frame):
// where the wall is cut open so the entry travel does not cross the printed wall.
// Filled by compute_wall_skip_points() once the plan is final.
std::vector<std::vector<Vec2f>> m_wall_skip_points;
bool m_enable_arc_fitting = false;
@@ -253,6 +276,7 @@ private:
} m_bed_shape;
float m_bed_width; // width of the bed bounding box
Vec2f m_bed_bottom_left; // bottom-left corner coordinates (for rectangular beds)
Polygon m_bed_polygon; // printable_area contour (scaled)
float m_perimeter_width = 0.4f * Width_To_Nozzle_Ratio; // Width of an extrusion line, also a perimeter spacing for 100% infill.
float m_extrusion_flow = 0.038f; //0.029f;// Extrusion flow is derived from m_perimeter_width, layer height and filament diameter.
@@ -278,6 +302,37 @@ private:
bool is_first_layer() const { return size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; }
// Purge row lattice of toolchange_Wipe(): row pitch and extrusion width.
float wipe_row_spacing(bool first_layer) const { return (first_layer ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; }
float wipe_line_width() const { return m_perimeter_width * m_extra_flow; }
// Whether toolchange_Unload() rams this (old) tool out.
bool tool_ramming_enabled(size_t tool) const { return (m_semm && m_enable_filament_ramming) || m_filpar[tool].multitool_ramming; }
// Whether the wipe restarts at the box boundary on a fresh row below the quantized
// ram band after ramming this (old) tool out (multi-tool gap wall; SEMM keeps the
// stock continue-from-ram-end behavior).
bool boundary_wipe_start_enabled(size_t tool) const { return tool_ramming_enabled(tool) && !m_semm && m_use_gap_wall; }
// With a boundary wipe start the wipe begins on a fresh row below the quantized ram
// band. Y offset from the box start to that first wipe row.
float wipe_start_offset_after_ram(float ramming_depth, bool first_layer) const
{
return ramming_depth + wipe_row_spacing(first_layer) - (m_perimeter_width + wipe_line_width()) / 2.f;
}
// Tower-local entry position of a toolchange whose box starts depth_traversed into
// the layer: the box corner, moved down to the first wipe row when the plan gives
// it a boundary wipe start (ramming_depth > 0 iff the unload rams). tool_change()
// enters here and compute_wall_skip_points() cuts the wall gap here, so the routed
// entry, the gap and the wipe scrub all share one opening.
Vec2f toolchange_entry_pos(float depth_traversed, float ramming_depth, bool first_layer) const
{
Vec2f pos(m_perimeter_width / 2.f, m_perimeter_width / 2.f + depth_traversed);
if (!m_semm && m_use_gap_wall && ramming_depth > 0.f)
pos.y() += wipe_start_offset_after_ram(ramming_depth, first_layer);
return pos;
}
// Calculates extrusion flow needed to produce required line width for given layer height
float extrusion_flow(float layer_height = -1.f) const // negative layer_height - return current m_extrusion_flow
{
@@ -328,9 +383,10 @@ private:
std::vector<float> m_used_filament_length;
std::vector<std::pair<float, std::vector<float>>> m_used_filament_length_until_layer;
// Return index of first toolchange that switches to non-soluble extruder
// ot -1 if there is no such toolchange.
int first_toolchange_to_nonsoluble(
// Return the index of the toolchange whose new filament should print the layer's
// finish extrusions (sparse infill + wall + brim), or -1 to print them with the
// layer's incoming filament before any toolchange happens.
int first_toolchange_to_nonsoluble_nonsupport(
const std::vector<WipeTowerInfo::ToolChange>& tool_changes) const;
void toolchange_Unload(
@@ -343,7 +399,9 @@ private:
void toolchange_Change(
WipeTowerWriter2 &writer,
const size_t new_tool,
const std::string& new_material);
const std::string& new_material,
const int wait_for_temp,
const bool wait_beside_tower);
void toolchange_Load(
WipeTowerWriter2 &writer,
@@ -353,7 +411,9 @@ private:
WipeTowerWriter2 &writer,
const WipeTower::box_coordinates &cleaning_box,
float wipe_volume,
bool interface_layer);
bool interface_layer,
bool priming = false,
bool fill_box = false);
Polygon generate_support_rib_wall(WipeTowerWriter2& writer,
@@ -361,8 +421,7 @@ private:
double feedrate,
bool first_layer,
bool rib_wall,
bool extrude_perimeter,
bool skip_points);
bool extrude_perimeter);
Polygon generate_support_cone_wall(
WipeTowerWriter2& writer,
@@ -372,6 +431,12 @@ private:
float spacing);
Polygon generate_rib_polygon(const WipeTower::box_coordinates& wt_box);
void compute_wall_skip_points();
// Computes the depth reserved for a toolchange (shared by plan_toolchange() and the
// rib-wall square-tower replanning in generate()).
WipeTowerInfo::ToolChange set_toolchange(size_t old_tool, size_t new_tool, float layer_height, float wipe_volume, bool first_layer_plan);
};
+180 -59
View File
@@ -2,6 +2,9 @@
#include "CustomGCode.hpp"
#include "I18N.hpp"
#include "PrintConfig.hpp"
#include "ClipperUtils.hpp"
#include "Geometry/ArcWelder.hpp"
#include "Line.hpp"
#include <algorithm>
#include <iomanip>
#include <iostream>
@@ -99,9 +102,87 @@ void GCodeWriter::apply_print_config(const PrintConfig &print_config)
m_max_jerk_z = LIMITS(machine_max_jerk_z);
m_max_jerk_e = LIMITS(machine_max_jerk_e);
m_resolution = print_config.resolution.value;
#undef LIMITS
#undef LIMITS_UINT
// Orca: capture the printable area(s) so a spiral lift can be skipped when its
// circle would leave the boundary and collide with the print limits. Full polygons
// are stored (not a bounding box) so the check stays correct for non-rectangular
// beds, and per-extruder areas are kept so printers with different boundaries per
// extruder use the right limit for whichever extruder is active.
auto to_scaled_polygon = [](const Pointfs &pts) {
Polygon poly;
poly.points.reserve(pts.size());
for (const Vec2d &p : pts)
poly.points.emplace_back(coord_t(scale_(p.x())), coord_t(scale_(p.y())));
poly.make_counter_clockwise();
return poly;
};
m_bed_printable_area.points.clear();
m_extruder_printable_areas.clear();
if (print_config.printable_area.values.size() >= 3)
m_bed_printable_area = to_scaled_polygon(print_config.printable_area.values);
const std::vector<Pointfs> &extruder_areas = print_config.extruder_printable_area.values;
if (!extruder_areas.empty()) {
m_extruder_printable_areas.resize(extruder_areas.size());
for (size_t i = 0; i < extruder_areas.size(); ++i) {
if (extruder_areas[i].size() < 3) {
// No dedicated area for this extruder: it can reach the whole bed.
m_extruder_printable_areas[i] = m_bed_printable_area;
continue;
}
Polygon extruder_poly = to_scaled_polygon(extruder_areas[i]);
if (m_bed_printable_area.points.size() < 3) {
m_extruder_printable_areas[i] = std::move(extruder_poly);
continue;
}
// The reachable area is the extruder area clipped to the bed. Bed shapes are
// convex in practice, so keep the largest resulting contour.
Polygons clipped = intersection(extruder_poly, m_bed_printable_area);
const Polygon *largest = nullptr;
double best_area = 0.;
for (const Polygon &p : clipped) {
double a = std::abs(p.area());
if (a > best_area) { best_area = a; largest = &p; }
}
m_extruder_printable_areas[i] = largest ? *largest : std::move(extruder_poly);
}
}
}
const Polygon *GCodeWriter::active_printable_area() const
{
if (const Extruder *e = this->filament()) {
size_t id = e->extruder_id();
if (id < m_extruder_printable_areas.size() && m_extruder_printable_areas[id].points.size() >= 3)
return &m_extruder_printable_areas[id];
}
if (m_bed_printable_area.points.size() >= 3)
return &m_bed_printable_area;
return nullptr;
}
bool GCodeWriter::spiral_lift_fits_printable_area(const Vec2d &center, double radius) const
{
const Polygon *area = this->active_printable_area();
if (area == nullptr)
return true; // Boundary unknown: don't restrict (preserve previous behavior).
const Point c = Point::new_scale(center.x(), center.y());
const double r_scaled = scale_(radius);
const double r2 = r_scaled * r_scaled;
// The spiral traces a full circle of `radius` around `center`, so the center must lie
// inside the printable area and every edge must be at least `radius` away from it.
if (!area->contains(c))
return false;
const Points &pts = area->points;
for (size_t i = 0, n = pts.size(); i < n; ++i)
if (Line::distance_to_squared(c, pts[i], pts[(i + 1) % n]) < r2)
return false;
return true;
}
void GCodeWriter::set_extruders(std::vector<unsigned int> extruder_ids)
@@ -110,6 +191,7 @@ void GCodeWriter::set_extruders(std::vector<unsigned int> extruder_ids)
m_filament_extruders.clear();
//ORCA: Reset current extruder ID and clear pointers to prevent dangling pointers when extruders are recreated.
m_curr_extruder_id = -1;
m_cached_extruder_idx = 0;
std::fill(m_curr_filament_extruder.begin(), m_curr_filament_extruder.end(), nullptr);
m_filament_extruders.reserve(extruder_ids.size());
for (unsigned int extruder_id : extruder_ids)
@@ -594,36 +676,36 @@ std::string GCodeWriter::update_progress(unsigned int num, unsigned int tot, boo
std::string GCodeWriter::toolchange_prefix() const
{
std::string gcode = "T";
// Orca: the manual-filament-change tag must stay ahead of the flavor selection so
// MMU manual-change handling keeps working.
if (config.manual_filament_change)
gcode = ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Manual_Tool_Change) + "T";
else {
if (m_is_bbl_printers)
gcode = "M1020 S";
else {
if (FLAVOR_IS(gcfMakerWare))
gcode = "M135 T";
else if (FLAVOR_IS(gcfSailfish))
gcode = "M108 T";
}
}
return gcode;
return ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Manual_Tool_Change) + "T";
return FLAVOR_IS(gcfMakerWare) ? "M135 T" :
FLAVOR_IS(gcfSailfish) ? "M108 T" : "T";
}
std::string GCodeWriter::toolchange(unsigned int filament_id)
std::string GCodeWriter::toolchange(unsigned int filament_id, int nozzle_id)
{
// set the new extruder
auto filament_extruder_iter = Slic3r::lower_bound_by_predicate(m_filament_extruders.begin(), m_filament_extruders.end(), [filament_id](const Extruder &e) { return e.id() < filament_id; });
assert(filament_extruder_iter != m_filament_extruders.end() && filament_extruder_iter->id() == filament_id);
m_curr_extruder_id = filament_extruder_iter->extruder_id();
m_curr_filament_extruder[m_curr_extruder_id] = &*filament_extruder_iter;
m_cached_extruder_idx = get_extruder_index(this->config, filament_id);
// return the toolchange command
// if we are running a single-extruder setup, just set the extruder and return nothing
std::ostringstream gcode;
// Orca: also emit for non-BBL single-extruder multi-filament setups (MMU-style).
if (this->multiple_extruders || (this->config.filament_diameter.values.size() > 1 && !is_bbl_printers())) {
// Orca: call toolchange_prefix() to get the correct command prefix based on the configuration and flavor.
gcode << this->toolchange_prefix() << filament_id;
// Orca: manual filament change keeps its tag line even on BBL machines, so the
// M1020 form must not shadow it. nozzle_id is signed: the null-safe nozzle
// lookup legitimately yields -1 ("no specific nozzle"), matching the literal
// H-1 the stock change templates emit; an unsigned would wrap.
if (m_is_bbl_printers && !config.manual_filament_change)
gcode << "M1020 S" << filament_id << " H" << nozzle_id;
else
gcode << this->toolchange_prefix() << filament_id;
if (GCodeWriter::full_gcode_comment)
gcode << " ; change extruder";
gcode << "\n";
@@ -632,6 +714,25 @@ std::string GCodeWriter::toolchange(unsigned int filament_id)
return gcode.str();
}
// Current parked-retract length of the filament's extruder, share-aware. m_filament_extruders is
// sorted by id (see toolchange), so a lower_bound lookup finds the entry; unknown filament ids
// degrade to 0 rather than dereferencing end().
double GCodeWriter::get_extruder_retracted_length(const int filament_id)
{
double res = 0.0;
auto filament_extruder_iter = Slic3r::lower_bound_by_predicate(m_filament_extruders.begin(), m_filament_extruders.end(),
[filament_id](const Extruder &e) { return (int) e.id() < filament_id; });
if (filament_extruder_iter == m_filament_extruders.end() || (int) filament_extruder_iter->id() != filament_id)
return res;
if (filament_extruder_iter->is_share_extruder())
res = filament_extruder_iter->get_share_retracted_length();
else
res = filament_extruder_iter->get_single_retracted_length();
return res;
}
std::string GCodeWriter::set_speed(double F, const std::string &comment, const std::string &cooling_marker)
{
assert(F > 0.);
@@ -658,7 +759,7 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com
GCodeG1Formatter w;
w.emit_xy(point_on_plate);
auto speed = m_is_first_layer
? this->config.get_abs_value_at("initial_layer_travel_speed", get_extruder_index(this->config, filament()->id())) : this->config.travel_speed.get_at(get_extruder_index(this->config, filament()->id()));
? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx);
w.emit_f(speed * 60.0);
//BBS
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
@@ -711,14 +812,19 @@ std::string GCodeWriter::eager_lift(const LiftType type) {
}
// BBS: spiral lift only safe with known position
// TODO: check the arc will move within bed area
if (type == LiftType::SpiralLift && this->is_current_position_clear()) {
double radius = target_lift / (2 * PI * atan(filament()->travel_slope()));
// static spiral alignment when no move in x,y plane.
// spiral centra is a radius distance to the right (y=0)
// spiral centra is a radius distance to the right (y=0)
Vec2d ij_offset = { radius, 0 };
if (target_lift > 0) {
// Orca: keep the spiral inside the active extruder's printable area, otherwise
// fall back to a normal lift to avoid colliding with the print boundary. m_pos
// includes the plate offset, so remove it to match the printable area coordinates.
const Vec2d spiral_center = { m_pos.x() - m_x_offset + ij_offset.x(), m_pos.y() - m_y_offset + ij_offset.y() };
if (target_lift > 0 && this->spiral_lift_fits_printable_area(spiral_center, radius)) {
lift_move = this->_spiral_travel_to_z(m_pos(2) + target_lift, ij_offset, "spiral lift Z");
} else if (target_lift > 0) {
lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z");
}
}
//BBS: if position is unknown use normal lift
@@ -744,7 +850,7 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
// BBS
Vec3d dest_point = point;
auto travel_speed =
m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", get_extruder_index(this->config, filament()->id())) : this->config.travel_speed.get_at(get_extruder_index(this->config, filament()->id()));
m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx);
//BBS: a z_hop need to be handle when travel
if (std::abs(m_to_lift) > EPSILON) {
assert(std::abs(m_lifted) < EPSILON);
@@ -773,7 +879,15 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
double radius = delta(2) / (2 * PI * atan(this->filament()->travel_slope()));
Vec2d ij_offset = radius * delta_no_z.normalized();
ij_offset = { -ij_offset(1), ij_offset(0) };
slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z");
// Orca: only perform the spiral lift if its full circle stays inside the
// printable area of the active extruder, otherwise fall back to a normal
// lift to avoid colliding with the print boundary. `source` is already in
// bed coordinates (plate offset removed), matching the printable area.
const Vec2d spiral_center = { source.x() + ij_offset.x(), source.y() + ij_offset.y() };
if (this->spiral_lift_fits_printable_area(spiral_center, radius))
slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z");
else
slop_move = _travel_to_z(target.z(), "normal lift Z");
}
//BBS: SlopeLift
else if (m_to_lift_type == LiftType::SlopeLift &&
@@ -841,13 +955,13 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
{
//force to move xy first then z after filament change
w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y()));
w.emit_f(this->config.travel_speed.get_at(get_extruder_index(this->config, filament()->id())) * 60.0);
w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
out_string = w.string() + _travel_to_z(point_on_plate.z(), comment);
} else {
GCodeG1Formatter w;
w.emit_xyz(point_on_plate);
w.emit_f(this->config.travel_speed.get_at(get_extruder_index(this->config, filament()->id())) * 60.0);
w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
out_string = w.string();
}
@@ -880,10 +994,10 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment)
{
m_pos(2) = z;
double speed = this->config.travel_speed_z.get_at(get_extruder_index(this->config, filament()->id()));
double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx);
if (speed == 0.) {
speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", get_extruder_index(this->config, filament()->id()))
: this->config.travel_speed.get_at(get_extruder_index(this->config, filament()->id()));
speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
}
GCodeG1Formatter w;
@@ -897,53 +1011,56 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment)
std::string GCodeWriter::_spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment)
{
std::string output;
double speed = this->config.travel_speed_z.get_at(get_extruder_index(this->config, filament()->id()));
double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx);
if (speed == 0.) {
speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", get_extruder_index(this->config, filament()->id()))
: this->config.travel_speed.get_at(get_extruder_index(this->config, filament()->id()));
speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
}
if (!this->config.enable_arc_fitting) { // Orca: if arc fitting is disabled, approximate the arc with small linear segments
std::ostringstream oss;
const double z_start = m_pos(2); // starting Z height
// --------------------------------------------------------------------
// Determine number of segments based on Resolution
// --------------------------------------------------------------------
const double ref_resolution = 0.01; // reference resolution in mm
const double ref_segments = 8.0; // reference number of segments at reference resolution
// number of linear segments to use for approximating the arc, clamp between 4 and 16
const int segments = std::clamp(int(std::round(ref_segments * (ref_resolution / m_resolution))), 4, 16);
// --------------------------------------------------------------------
const double px = m_pos(0) - m_x_offset; // take plate offset into consideration
const double py = m_pos(1) - m_y_offset; // take plate offset into consideration
const double cx = px + ij_offset(0); // center x
const double cy = py + ij_offset(1); // center y
const double radius = ij_offset.norm(); // radius
// Number of linear segments approximating the circle, chosen so that a chord never deviates
// from the true arc by more than the slicing resolution. A resolution of 0 means "no
// simplification", which has no finite segment count, so it takes the upper bound.
constexpr size_t min_segments = 8; // keep a small spiral visibly round
constexpr size_t max_segments = 128; // bound the emitted G-code
const int segments = int(m_resolution > 0. ?
std::clamp(Geometry::ArcWelder::arc_discretization_steps(radius, 2. * M_PI, m_resolution), min_segments, max_segments) :
max_segments);
const double a0 = std::atan2(py - cy, px - cx); // start angle
const double delta = 2.0 * M_PI; // CCW full circle
if (full_gcode_comment)
oss << ";" << comment << "\n";
auto emit_point = [&output](const Vec3d &point) {
GCodeG1Formatter w;
w.emit_xyz(point);
output += w.string();
};
oss << "G1 F" << (speed * 60.0) << "\n"; // set feedrate
output.reserve(size_t(segments) * 40); // ~40 characters per emitted G1 line
GCodeG1Formatter w; // set feedrate
w.emit_f(speed * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
output += w.string();
// approximate the arc with small linear segments (without the last point which is added later to ensure exactness)
for (int i = 1; i < segments; ++i) {
double t = double(i) / segments; // parametric position along arc
double a = a0 + delta * t; // CCW arc param
double x = cx + radius * std::cos(a); // point on circle
double y = cy + radius * std::sin(a); // point on circle
double zz = z_start + (z - z_start) * t; // interpolated Z height
oss << "G1 X" << x << " Y" << y << " Z" << zz << "\n";
const double t = double(i) / segments; // parametric position along arc
const double a = a0 + 2. * M_PI * t; // CCW arc param, full circle
emit_point(Vec3d(cx + radius * std::cos(a), // point on circle
cy + radius * std::sin(a),
z_start + (z - z_start) * t)); // interpolated Z height
}
oss << "G1 X" << px << " Y" << py << " Z" << z << "\n"; // final point to ensure exactness
output = oss.str();
emit_point(Vec3d(px, py, z)); // final point to ensure exactness
} else { // Orca: if arc fitting is enabled emit a G2/G3 command for the spiral lift
output = std::string("G17") + (full_gcode_comment ? " ; XY plane for arc\n" : "\n");
@@ -1101,7 +1218,7 @@ std::string GCodeWriter::_retract(double length, double restart_extra, const std
return gcode;
}
std::string GCodeWriter::unretract()
std::string GCodeWriter::unretract(float extra_retract)
{
std::string gcode;
@@ -1117,7 +1234,9 @@ std::string GCodeWriter::unretract()
//BBS
// use G1 instead of G0 because G0 will blend the restart with the previous travel move
GCodeG1Formatter w;
w.emit_e(filament()->E());
// extra_retract over-extrudes for the PETG pre-extrusion; 0 by
// default -> identical to the plain deretract E position.
w.emit_e(filament()->E() + extra_retract);
w.emit_f(filament()->deretract_speed() * 60.);
//BBS
w.emit_comment(GCodeWriter::full_gcode_comment, " ; unretract");
@@ -1250,8 +1369,9 @@ std::string GCodeWriter::set_extruder(unsigned int filament_id)
auto filament_ext_it = Slic3r::lower_bound_by_predicate(m_filament_extruders.begin(), m_filament_extruders.end(), [filament_id](const Extruder &e) { return e.id() < filament_id; });
unsigned int extruder_id = filament_ext_it->extruder_id();
assert(filament_ext_it != m_filament_extruders.end() && filament_ext_it->id() == filament_id);
//TODO: optmize here, pass extruder_id to toolchange
return this->need_toolchange(filament_id) ? this->toolchange(filament_id) : "";
// Orca: writer-only context (calibration paths) has no nozzle grouping; the
// filament's own extruder id is the correct degenerate nozzle value.
return this->need_toolchange(filament_id) ? this->toolchange(filament_id, (int) extruder_id) : "";
}
void GCodeWriter::init_extruder(unsigned int filament_id)
@@ -1261,6 +1381,7 @@ void GCodeWriter::init_extruder(unsigned int filament_id)
assert(filament_extruder_iter != m_filament_extruders.end() && filament_extruder_iter->id() == filament_id);
m_curr_extruder_id = filament_extruder_iter->extruder_id();
m_curr_filament_extruder[m_curr_extruder_id] = &*filament_extruder_iter;
m_cached_extruder_idx = get_extruder_index(this->config, filament_id);
}
}
+25 -2
View File
@@ -6,6 +6,7 @@
#include <charconv>
#include "Extruder.hpp"
#include "Point.hpp"
#include "Polygon.hpp"
#include "PrintConfig.hpp"
#include "GCode/CoolingBuffer.hpp"
@@ -19,6 +20,7 @@ public:
GCodeWriter() :
multiple_extruders(false), m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr),
m_curr_extruder_id (-1),
m_cached_extruder_idx(0),
m_single_extruder_multi_material(false),
m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0),
m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0),
@@ -66,10 +68,13 @@ public:
bool need_toolchange(unsigned int filament_id) const;
std::string set_extruder(unsigned int filament_id);
void init_extruder(unsigned int filament_id);
// Current parked-retract length of a filament's extruder (share-aware). Used for the
// new_extruder_retracted_length change-filament placeholder. Returns 0 if the filament is unknown.
double get_extruder_retracted_length(const int filament_id);
// Prefix of the toolchange G-code line, to be used by the CoolingBuffer to separate sections of the G-code
// printed with the same extruder.
std::string toolchange_prefix() const;
std::string toolchange(unsigned int filament_id);
std::string toolchange(unsigned int filament_id, int nozzle_id);
std::string set_speed(double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string());
// SoftFever NOTE: the returned speed is mm/minute
double get_current_speed() const { return m_current_speed;}
@@ -83,7 +88,9 @@ public:
std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
std::string retract(bool before_wipe = false, double retract_length = 0);
std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0);
std::string unretract();
// extra_retract adds a small over-extrusion to the deretract move (PETG pre-extrusion).
// Default 0 -> byte-identical to the plain deretract.
std::string unretract(float extra_retract = 0.f);
// do lift instantly
std::string eager_lift(const LiftType type);
// record a lift request, do realy lift in next travel
@@ -126,6 +133,8 @@ public:
const bool is_bbl_printers() const {return m_is_bbl_printers;}
void set_is_first_layer(bool bval) { m_is_first_layer = bval; }
GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; }
void invalidate_acceleration() { m_last_acceleration = 0; m_last_travel_acceleration = 0; }
void invalidate_jerk() { m_last_jerk = 0; }
// Returns whether this flavor supports separate print and travel acceleration.
static bool supports_separate_travel_acceleration(GCodeFlavor flavor);
@@ -135,6 +144,8 @@ public:
bool m_single_extruder_multi_material;
std::vector<Extruder*> m_curr_filament_extruder;
int m_curr_extruder_id;
// Motion uses the global/base process variant until a filament becomes active.
size_t m_cached_extruder_idx;
unsigned int m_last_acceleration;
unsigned int m_last_travel_acceleration;
std::vector<unsigned int> m_max_travel_acceleration;
@@ -173,6 +184,14 @@ public:
// Orca: slicing resolution in mm
double m_resolution = 0.01;
// Orca: printable area polygons (scaled, bed coordinates) used to keep spiral lifts
// from colliding with the print boundary. m_extruder_printable_areas holds the
// per-extruder reachable area (intersected with the bed) when a printer defines
// different boundaries per extruder; m_bed_printable_area is the global fallback.
// Storing full polygons (rather than a bounding box) keeps the check correct for
// non-rectangular beds such as delta/circular printers.
Polygon m_bed_printable_area;
std::vector<Polygon> m_extruder_printable_areas;
std::string m_gcode_label_objects_start;
std::string m_gcode_label_objects_end;
@@ -189,6 +208,10 @@ public:
std::string _travel_to_z(double z, const std::string &comment);
std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment);
// Orca: printable area of the active extruder (per-extruder when configured, otherwise the bed). Null when unknown.
const Polygon *active_printable_area() const;
// Orca: true if a full spiral-lift circle (center in bed coordinates, mm) fits inside the active printable area.
bool spiral_lift_fits_printable_area(const Vec2d &center, double radius) const;
std::string _retract(double length, double restart_extra, const std::string &comment);
std::string set_acceleration_internal(Acceleration type, unsigned int acceleration);
+4
View File
@@ -157,6 +157,10 @@ public:
ExPolygons lslices;
ExPolygons lslices_extrudable; // BBS: the extrudable part of lslices used for tree support
std::vector<BoundingBox> lslices_bboxes;
// Orca: for separated infills / per-model centering. Aligned with lslices: for each island, the
// full bounding box of the 3D connected body (across all layers) it belongs to. Populated by
// PrintObject::infill() only when the feature is used; empty otherwise.
std::vector<BoundingBox> lslices_separated_component_bboxes;
// BBS
ExPolygons loverhangs;
+1 -1
View File
@@ -94,7 +94,7 @@ public:
void* volume{nullptr};
std::vector<int>* plane_indices{nullptr};
Transform3d world_tran;
Transform3d world_tran = Transform3d::Identity();
std::shared_ptr<std::vector<SurfaceFeature>> world_plane_features{nullptr};
std::shared_ptr<SurfaceFeature> origin_surface_feature{nullptr};
+17 -10
View File
@@ -920,6 +920,13 @@ public:
// Extruder ID is only valid for FFF. Returns -1 for SLA or if the extruder ID is not applicable (support volumes).
int extruder_id() const;
//Orca: cache clearing procedure to ensure that the shape is positioned accurately when manipulating it
void clear_cache() {
m_cached_trans_matrix = Transform3d::Identity().inverse(); // get unvelivable matrix
m_convex_hull_2d.clear();
m_cached_2d_polygon.clear();
};
bool is_splittable() const;
// BBS
@@ -966,34 +973,34 @@ public:
static std::string type_to_string(const ModelVolumeType t);
const Geometry::Transformation& get_transformation() const { return m_transformation; }
void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; }
void set_transformation(const Transform3d& trafo) { m_transformation.set_matrix(trafo); }
void set_transformation(const Geometry::Transformation& transformation) { clear_cache(); m_transformation = transformation; }
void set_transformation(const Transform3d& trafo) { clear_cache(); m_transformation.set_matrix(trafo); }
Vec3d get_offset() const { return m_transformation.get_offset(); }
double get_offset(Axis axis) const { return m_transformation.get_offset(axis); }
void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); }
void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); }
void set_offset(const Vec3d& offset) { clear_cache(); m_transformation.set_offset(offset); }
void set_offset(Axis axis, double offset) { clear_cache(); m_transformation.set_offset(axis, offset); }
Vec3d get_rotation() const { return m_transformation.get_rotation(); }
double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); }
void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); }
void set_rotation(Axis axis, double rotation) { m_transformation.set_rotation(axis, rotation); }
void set_rotation(const Vec3d& rotation) { clear_cache(); m_transformation.set_rotation(rotation); }
void set_rotation(Axis axis, double rotation) { clear_cache(); m_transformation.set_rotation(axis, rotation); }
Vec3d get_scaling_factor() const { return m_transformation.get_scaling_factor(); }
double get_scaling_factor(Axis axis) const { return m_transformation.get_scaling_factor(axis); }
void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); }
void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); }
void set_scaling_factor(const Vec3d& scaling_factor) { clear_cache(); m_transformation.set_scaling_factor(scaling_factor); }
void set_scaling_factor(Axis axis, double scaling_factor) {clear_cache(); m_transformation.set_scaling_factor(axis, scaling_factor); }
Vec3d get_mirror() const { return m_transformation.get_mirror(); }
double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); }
bool is_left_handed() const { return m_transformation.is_left_handed(); }
void set_mirror(const Vec3d& mirror) { m_transformation.set_mirror(mirror); }
void set_mirror(Axis axis, double mirror) { m_transformation.set_mirror(axis, mirror); }
void set_mirror(const Vec3d& mirror) { clear_cache(); m_transformation.set_mirror(mirror); }
void set_mirror(Axis axis, double mirror) { clear_cache(); m_transformation.set_mirror(axis, mirror); }
void convert_from_imperial_units();
void convert_from_meters();
+13 -2
View File
@@ -1177,6 +1177,18 @@ static bool is_volume_sinking(const indexed_triangle_set &its, const Transform3d
//#define MMU_SEGMENTATION_DEBUG_TOP_BOTTOM
double resolve_outer_wall_line_width(const PrintRegionConfig &region_config, const PrintObjectConfig &object_config, const PrintConfig &print_config)
{
// A filament id of 0 underflows, and get_at() then falls back to the first nozzle.
const double nozzle_diameter = print_config.nozzle_diameter.get_at(region_config.outer_wall_filament_id - 1);
ConfigOptionFloatOrPercent width = region_config.outer_wall_line_width;
if (width.value == 0)
width = object_config.line_width;
if (!width.percent && width.value <= 0.)
return Flow::auto_extrusion_width(frExternalPerimeter, float(nozzle_diameter));
return width.get_abs_value(nozzle_diameter);
}
// Returns segmentation of top and bottom layers based on painting in segmentation gizmos.
static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_layers(const PrintObject &print_object,
const std::vector<ExPolygons> &input_expolygons,
@@ -1347,8 +1359,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
// As this region may split existing regions, we collect statistics over all regions for color_idx == 0.
color_idx == 0 || config.outer_wall_filament_id == int(color_idx)) {
//BBS: the extrusion line width is outer wall rather than inner wall
const double nozzle_diameter = print_object.print()->config().nozzle_diameter.get_at(0);
double outer_wall_line_width = config.get_abs_value("outer_wall_line_width", nozzle_diameter);
double outer_wall_line_width = resolve_outer_wall_line_width(config, print_object.config(), print_object.print()->config());
out.extrusion_width = std::max<float>(out.extrusion_width, outer_wall_line_width);
out.top_shell_layers = std::max<int>(out.top_shell_layers, config.top_shell_layers);
out.bottom_shell_layers = std::max<int>(out.bottom_shell_layers, config.bottom_shell_layers);
@@ -9,6 +9,9 @@ namespace Slic3r {
class ExPolygon;
class ModelVolume;
class PrintObject;
class PrintConfig;
class PrintObjectConfig;
class PrintRegionConfig;
class FacetsAnnotation;
using ExPolygons = std::vector<ExPolygon>;
@@ -52,6 +55,9 @@ std::vector<std::vector<ExPolygons>> multi_material_segmentation_by_painting(con
// Returns fuzzy skin segmentation based on painting in fuzzy skin segmentation gizmo
std::vector<std::vector<ExPolygons>> fuzzy_skin_segmentation_by_painting(const PrintObject &print_object, const std::function<void()> &throw_on_cancel_callback);
// Effective outer-wall line width for a region, resolved against its own nozzle with PrintRegion::flow's fallback.
double resolve_outer_wall_line_width(const PrintRegionConfig &region_config, const PrintObjectConfig &object_config, const PrintConfig &print_config);
} // namespace Slic3r
namespace boost::polygon {
+970
View File
@@ -0,0 +1,970 @@
#include "MultiNozzleUtils.hpp"
#include "Utils.hpp"
#include "ProjectTask.hpp" // Slic3r::FilamentInfo (StaticNozzleGroupResult / load_nozzle_infos_with_compatibility)
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
#include <boost/log/trivial.hpp>
// Multi-nozzle support.
namespace Slic3r { namespace MultiNozzleUtils {
// ==================== tool function implementations ====================
std::vector<NozzleInfo> build_nozzle_list(std::vector<NozzleGroupInfo> nozzle_groups)
{
std::vector<NozzleInfo> ret;
std::sort(nozzle_groups.begin(), nozzle_groups.end());
int nozzle_id = 0;
for (auto& group : nozzle_groups) {
for (int i = 0; i < group.nozzle_count; ++i) {
NozzleInfo tmp;
tmp.diameter = group.diameter;
tmp.extruder_id = group.extruder_id;
tmp.volume_type = group.volume_type;
tmp.group_id = nozzle_id++;
ret.emplace_back(std::move(tmp));
}
}
return ret;
}
std::vector<NozzleInfo> build_nozzle_list(double diameter, const std::vector<int>& filament_nozzle_map, const std::vector<int>& filament_volume_map, const std::vector<int>& filament_map)
{
std::string diameter_str = format_diameter_to_str(diameter);
std::map<int, std::vector<int>> nozzle_to_filaments;
for(size_t idx = 0; idx < filament_nozzle_map.size(); ++idx){
int nozzle_id = filament_nozzle_map[idx];
nozzle_to_filaments[nozzle_id].emplace_back(static_cast<int>(idx));
}
std::vector<NozzleInfo> ret;
for(auto& elem : nozzle_to_filaments){
int nozzle_id = elem.first;
auto& filaments = elem.second;
NozzleInfo info;
info.diameter = diameter_str;
info.group_id = nozzle_id;
info.extruder_id = filament_map[filaments.front()];
info.volume_type = NozzleVolumeType(filament_volume_map[filaments.front()]);
ret.emplace_back(std::move(info));
}
return ret;
}
void normalize_nozzle_map_per_layer(std::vector<std::vector<int>> &layer_filament_nozzle_maps,
const std::vector<std::vector<unsigned int>> &layer_filaments)
{
if (layer_filament_nozzle_maps.empty())
return;
const int total_layers = static_cast<int>(layer_filament_nozzle_maps.size());
int filament_count = 0;
for (const auto &layer_map : layer_filament_nozzle_maps)
filament_count = std::max(filament_count, static_cast<int>(layer_map.size()));
auto layer_uses_filament = [](const std::vector<unsigned int> &filaments, int filament_id) {
return std::find(filaments.begin(), filaments.end(), static_cast<unsigned int>(filament_id)) != filaments.end();
};
std::vector<int> last_used_nozzle(filament_count, -1);
std::unordered_map<int, int> first_used_nozzle;
std::unordered_map<int, int> first_used_layer;
// Forward pass: layers that extrude a filament define its nozzle; layers that don't inherit
// the nozzle it last used (carry-forward), remembering the first-ever nozzle for the back-fill.
for (int layer_id = 0; layer_id < total_layers; ++layer_id) {
auto &layer_map = layer_filament_nozzle_maps[layer_id];
const auto &used = layer_id < static_cast<int>(layer_filaments.size()) ? layer_filaments[layer_id] : std::vector<unsigned int>();
for (int filament_id = 0; filament_id < static_cast<int>(layer_map.size()); ++filament_id) {
if (layer_uses_filament(used, filament_id)) {
last_used_nozzle[filament_id] = layer_map[filament_id];
if (first_used_nozzle.count(filament_id) == 0) {
first_used_nozzle[filament_id] = layer_map[filament_id];
first_used_layer[filament_id] = layer_id;
}
} else if (last_used_nozzle[filament_id] >= 0) {
layer_map[filament_id] = last_used_nozzle[filament_id];
}
}
}
// Back-fill pass: layers before a filament's first use inherit the first nozzle it ever uses.
for (int layer_id = 0; layer_id < total_layers; ++layer_id) {
auto &layer_map = layer_filament_nozzle_maps[layer_id];
for (int filament_id = 0; filament_id < static_cast<int>(layer_map.size()); ++filament_id) {
if (first_used_layer.count(filament_id) != 0 && layer_id < first_used_layer[filament_id])
layer_map[filament_id] = first_used_nozzle[filament_id];
}
}
}
// ==================== LayeredNozzleGroupResult ====================
static bool has_filament_mapped_to_multiple_nozzles(const std::vector<std::vector<int>> &layer_filament_nozzle_maps,
const std::vector<unsigned int> &used_filaments)
{
if (layer_filament_nozzle_maps.empty() || used_filaments.empty())
return false;
for (auto filament_id_u : used_filaments) {
int filament_id = static_cast<int>(filament_id_u);
std::set<int> nozzle_ids;
for (size_t layer_id = 0; layer_id < layer_filament_nozzle_maps.size(); ++layer_id) {
const auto &map = layer_filament_nozzle_maps[layer_id];
if (filament_id < 0 || filament_id >= static_cast<int>(map.size()))
continue;
int nozzle_id = map[filament_id];
if (nozzle_id < 0)
continue;
nozzle_ids.insert(nozzle_id);
if (nozzle_ids.size() > 1)
return true;
}
}
return false;
}
std::optional<LayeredNozzleGroupResult> LayeredNozzleGroupResult::create(
const std::vector<int>& filament_nozzle_map,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<unsigned int>& used_filaments)
{
if (filament_nozzle_map.empty() || nozzle_list.empty()) {
return std::nullopt;
}
LayeredNozzleGroupResult result(false);
result._default_filament_nozzle_map = filament_nozzle_map;
result._nozzle_list = nozzle_list;
result._used_filaments = used_filaments;
return result;
}
std::optional<LayeredNozzleGroupResult> LayeredNozzleGroupResult::create(
const std::vector<std::vector<int>>& layer_filament_nozzle_maps,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<unsigned int>& used_filaments,
const std::vector<std::vector<unsigned int>>& layer_filament_sequences)
{
if (layer_filament_nozzle_maps.empty() || nozzle_list.empty()) {
return std::nullopt;
}
bool support_dynamic_nozzle_map = has_filament_mapped_to_multiple_nozzles(layer_filament_nozzle_maps, used_filaments);
LayeredNozzleGroupResult result(support_dynamic_nozzle_map);
result._layer_filament_nozzle_maps = layer_filament_nozzle_maps;
result._layer_filament_sequences = layer_filament_sequences;
result._nozzle_list = nozzle_list;
result._used_filaments = used_filaments;
if (!layer_filament_nozzle_maps.empty()) {
result._default_filament_nozzle_map = layer_filament_nozzle_maps[0];
}
return result;
}
std::optional<LayeredNozzleGroupResult> LayeredNozzleGroupResult::create(
const std::vector<unsigned int>& used_filaments,
const std::vector<int>& filament_map,
const std::vector<int>& filament_volume_map,
const std::vector<int>& filament_nozzle_map,
const std::vector<std::map<NozzleVolumeType, int>> &nozzle_count,
float diameter)
{
std::vector<NozzleGroupInfo> nozzle_groups;
for (size_t extruder_id = 0; extruder_id < nozzle_count.size(); ++extruder_id) {
for (auto elem : nozzle_count[extruder_id]) {
NozzleGroupInfo group_info;
group_info.diameter = format_diameter_to_str(diameter);
group_info.volume_type = elem.first;
group_info.nozzle_count = elem.second;
group_info.extruder_id = static_cast<int>(extruder_id);
nozzle_groups.emplace_back(group_info);
}
}
auto nozzle_list = build_nozzle_list(nozzle_groups);
std::vector<bool> used_nozzle(nozzle_list.size(), false);
std::map<int, int> input_nozzle_id_to_output;
std::vector<int> output_nozzle_map(filament_nozzle_map.size(), 0);
for (auto filament_idx : used_filaments) {
NozzleVolumeType req_type = NozzleVolumeType(filament_volume_map[filament_idx]);
int req_extruder = filament_map[filament_idx];
int input_nozzle_idx = filament_nozzle_map[filament_idx];
if (input_nozzle_id_to_output.find(input_nozzle_idx) != input_nozzle_id_to_output.end()) {
output_nozzle_map[filament_idx] = input_nozzle_id_to_output[input_nozzle_idx];
continue;
}
int output_nozzle_idx = -1;
for (size_t nozzle_idx = 0; nozzle_idx < nozzle_list.size(); ++nozzle_idx) {
if (used_nozzle[nozzle_idx]) continue;
auto &nozzle_info = nozzle_list[nozzle_idx];
if (!(nozzle_info.extruder_id == req_extruder && nozzle_info.volume_type == req_type)) continue;
output_nozzle_idx = static_cast<int>(nozzle_idx);
input_nozzle_id_to_output[input_nozzle_idx] = output_nozzle_idx;
used_nozzle[nozzle_idx] = true;
break;
}
if (output_nozzle_idx == -1) { return std::nullopt; }
output_nozzle_map[filament_idx] = output_nozzle_idx;
}
return create(output_nozzle_map, nozzle_list, used_filaments);
}
bool LayeredNozzleGroupResult::are_filaments_same_extruder(int filament_id1, int filament_id2, int layer_id) const
{
std::optional<NozzleInfo> nozzle_info1 = get_nozzle_for_filament(filament_id1, layer_id);
std::optional<NozzleInfo> nozzle_info2 = get_nozzle_for_filament(filament_id2, layer_id);
if (!nozzle_info1 || !nozzle_info2) return false;
return nozzle_info1->extruder_id == nozzle_info2->extruder_id;
}
bool LayeredNozzleGroupResult::are_filaments_same_nozzle(int filament_id1, int filament_id2, int layer_id) const
{
std::optional<NozzleInfo> nozzle_info1 = get_nozzle_for_filament(filament_id1, layer_id);
std::optional<NozzleInfo> nozzle_info2 = get_nozzle_for_filament(filament_id2, layer_id);
if (!nozzle_info1 || !nozzle_info2) return false;
return nozzle_info1->group_id == nozzle_info2->group_id;
}
int LayeredNozzleGroupResult::get_extruder_count() const
{
std::set<int> extruder_ids;
for (const auto &nozzle : _nozzle_list) { extruder_ids.insert(nozzle.extruder_id); }
return static_cast<int>(extruder_ids.size());
}
std::vector<NozzleInfo> LayeredNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id) const
{
return get_used_nozzles_in_extruder(target_extruder_id, -1);
}
std::vector<NozzleInfo> LayeredNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id, int layer_id) const
{
std::set<int> nozzle_ids;
std::vector<NozzleInfo> result;
std::vector<unsigned int> target_filaments = get_used_filaments(layer_id);
for (unsigned int filament_id : target_filaments) {
if (layer_id != -1) {
auto nozzle_opt = get_nozzle_for_filament(static_cast<int>(filament_id), layer_id);
if (nozzle_opt) {
if (target_extruder_id == -1 || nozzle_opt->extruder_id == target_extruder_id) { nozzle_ids.insert(nozzle_opt->group_id); }
}
} else {
auto nozzles = get_nozzles_for_filament(static_cast<int>(filament_id));
for (const auto &nozzle : nozzles) {
if (target_extruder_id == -1 || nozzle.extruder_id == target_extruder_id) { nozzle_ids.insert(nozzle.group_id); }
}
}
}
for (int nozzle_id : nozzle_ids) {
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(_nozzle_list.size())) { result.push_back(_nozzle_list[nozzle_id]); }
}
return result;
}
std::vector<int> LayeredNozzleGroupResult::get_used_extruders() const
{
return get_used_extruders(-1);
}
std::vector<int> LayeredNozzleGroupResult::get_used_extruders(int layer_id) const
{
std::set<int> used_extruders;
// used filaments on the given layer (or globally)
std::vector<unsigned int> target_filaments = get_used_filaments(layer_id);
for (auto filament_id : target_filaments) {
if (layer_id != -1) {
// single-layer: nozzle used by this filament on this layer
auto nozzle_opt = get_nozzle_for_filament(static_cast<int>(filament_id), layer_id);
if (nozzle_opt) { used_extruders.insert(nozzle_opt->extruder_id); }
} else {
// global: every nozzle this filament uses across all layers
auto nozzles = get_nozzles_for_filament(static_cast<int>(filament_id));
for (const auto &nozzle : nozzles) { used_extruders.insert(nozzle.extruder_id); }
}
}
return std::vector<int>(used_extruders.begin(), used_extruders.end());
}
std::vector<int> LayeredNozzleGroupResult::get_extruder_map(bool zero_based, int layer_id) const
{
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
std::vector<int> extruder_map(filament_nozzle_map.size());
for (size_t idx = 0; idx < filament_nozzle_map.size(); ++idx) {
int nozzle_id = filament_nozzle_map[idx];
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(_nozzle_list.size())) {
extruder_map[idx] = _nozzle_list[nozzle_id].extruder_id;
} else {
extruder_map[idx] = -1;
}
}
if (zero_based) return extruder_map;
auto new_filament_map = extruder_map;
std::transform(new_filament_map.begin(), new_filament_map.end(), new_filament_map.begin(), [](int val) { return val + 1; });
return new_filament_map;
}
std::vector<int> LayeredNozzleGroupResult::get_nozzle_map(int layer_id) const
{
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
std::vector<int> nozzle_map(filament_nozzle_map.size());
for (size_t idx = 0; idx < filament_nozzle_map.size(); ++idx) {
int nozzle_id = filament_nozzle_map[idx];
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(_nozzle_list.size())) {
nozzle_map[idx] = _nozzle_list[nozzle_id].group_id;
} else {
nozzle_map[idx] = -1;
}
}
return nozzle_map;
}
std::vector<int> LayeredNozzleGroupResult::get_volume_map(int layer_id) const
{
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
std::vector<int> volume_map(filament_nozzle_map.size());
for (size_t idx = 0; idx < filament_nozzle_map.size(); ++idx) {
int nozzle_id = filament_nozzle_map[idx];
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(_nozzle_list.size())) {
volume_map[idx] = _nozzle_list[nozzle_id].volume_type;
} else {
volume_map[idx] = -1;
}
}
return volume_map;
}
std::vector<unsigned int> LayeredNozzleGroupResult::get_used_filaments(int layer_id) const
{
if (layer_id < 0) { return _used_filaments; }
if (layer_id >= static_cast<int>(_layer_filament_nozzle_maps.size())) { return _used_filaments; }
if (!_layer_filament_sequences.empty() && layer_id < static_cast<int>(_layer_filament_sequences.size())) {
return _layer_filament_sequences[layer_id];
}
return {};
}
std::optional<NozzleInfo> LayeredNozzleGroupResult::get_nozzle_for_filament(int filament_id, int layer_id) const
{
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
if (filament_id < 0 || filament_id >= static_cast<int>(filament_nozzle_map.size())) { return std::nullopt; }
int nozzle_id = filament_nozzle_map[filament_id];
return get_nozzle_from_id(nozzle_id);
}
std::vector<NozzleInfo> LayeredNozzleGroupResult::get_nozzles_for_filament(int filament_id) const
{
std::set<int> nozzle_ids;
if (!support_dynamic_nozzle_map) {
if (filament_id >= 0 && filament_id < static_cast<int>(_default_filament_nozzle_map.size())) {
nozzle_ids.insert(_default_filament_nozzle_map[filament_id]);
}
} else {
int start_layer = 0;
int end_layer = static_cast<int>(_layer_filament_nozzle_maps.size());
for (int i = start_layer; i < end_layer; ++i) {
const auto &map = _layer_filament_nozzle_maps[i];
if (filament_id >= 0 && filament_id < static_cast<int>(map.size())) {
nozzle_ids.insert(map[filament_id]);
}
}
}
std::vector<NozzleInfo> result;
for (int id : nozzle_ids) {
if (id >= 0 && id < static_cast<int>(_nozzle_list.size())) { result.push_back(_nozzle_list[id]); }
}
return result;
}
std::optional<NozzleInfo> LayeredNozzleGroupResult::get_first_nozzle_for_filament(int filament_id) const
{
if (filament_id < 0) return std::nullopt;
if (!support_dynamic_nozzle_map) {
if (filament_id >= static_cast<int>(_default_filament_nozzle_map.size())) return std::nullopt;
return get_nozzle_from_id(_default_filament_nozzle_map[filament_id]);
}
for (size_t layer = 0; layer < _layer_filament_nozzle_maps.size(); ++layer) {
auto layer_used_filaments = get_used_filaments(layer);
if (std::find(layer_used_filaments.begin(), layer_used_filaments.end(), static_cast<unsigned int>(filament_id)) == layer_used_filaments.end()){
continue;
}
const auto &map = _layer_filament_nozzle_maps[layer];
if (filament_id >= 0 && filament_id < static_cast<int>(map.size())) {
int nozzle_id = map[filament_id];
auto nozzle = get_nozzle_from_id(nozzle_id);
if (nozzle) return nozzle;
}
}
return std::nullopt;
}
std::optional<NozzleInfo> LayeredNozzleGroupResult::get_nozzle_from_id(int nozzle_id) const
{
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(_nozzle_list.size())) { return std::nullopt; }
return _nozzle_list[nozzle_id];
}
int LayeredNozzleGroupResult::get_extruder_id(int filament_id, int layer_id) const
{
auto nozzle_info = get_nozzle_for_filament(filament_id, layer_id);
return nozzle_info ? nozzle_info->extruder_id : -1;
}
int LayeredNozzleGroupResult::get_nozzle_id(int filament_id, int layer_id) const
{
auto nozzle_info = get_nozzle_for_filament(filament_id, layer_id);
return nozzle_info ? nozzle_info->group_id : -1;
}
const std::vector<int> &LayeredNozzleGroupResult::get_layer_filament_nozzle_map(int layer_id) const
{
if (layer_id >= 0 && layer_id < static_cast<int>(_layer_filament_nozzle_maps.size())) { return _layer_filament_nozzle_maps[layer_id]; }
return _default_filament_nozzle_map;
}
// ==================== filament-change-time model ====================
FilamentChangeSimResult simulate_filament_change_time(
const std::vector<int>& logical_filaments,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<int>& filament_change_seq,
const std::vector<int>& nozzle_change_seq,
const std::vector<int>& group_of_filament,
const FilamentChangeTimeParams& time_params,
const std::vector<bool>& ams_preload_enabled,
bool calc_sliced_time)
{
FilamentChangeSimResult result;
if (logical_filaments.empty() || nozzle_list.empty() || filament_change_seq.empty() || nozzle_change_seq.empty())
return result;
// Re-map the parameter semantics:
// standard = AMS -> selector -> extruder (full path), selector = selector -> extruder (short path)
// so AMS -> selector = standard - selector
const float load_ams_to_selector = time_params.standard_load_time - time_params.selector_load_time;
const float unload_ams_to_selector = time_params.standard_unload_time - time_params.selector_unload_time;
const float load_selector_to_ext = time_params.selector_load_time;
const float unload_ext_to_selector = time_params.selector_unload_time;
// nozzle_id -> extruder_id
std::unordered_map<int, int> nozzle_to_extruder;
nozzle_to_extruder.reserve(nozzle_list.size());
for (const auto& nozzle : nozzle_list)
nozzle_to_extruder[nozzle.group_id] = nozzle.extruder_id;
// filament_id -> AMS group
std::unordered_map<int, int> filament_to_group;
filament_to_group.reserve(logical_filaments.size());
for (size_t i = 0; i < logical_filaments.size(); ++i)
filament_to_group[logical_filaments[i]] = group_of_filament[i];
const auto get_group = [&](int filament_id) -> int {
auto it = filament_to_group.find(filament_id);
return it != filament_to_group.end() ? it->second : -1;
};
const auto is_preload_enabled = [&](int group_id) -> bool {
if (group_id < 0 || group_id >= static_cast<int>(ams_preload_enabled.size()))
return false;
return ams_preload_enabled[group_id];
};
// Filament location states
enum class Location { IN_AMS, IN_SELECTOR, IN_EXTRUDER };
std::unordered_map<int, Location> filament_location; // filament_id -> current location
std::unordered_map<int, int> filament_extruder; // filament_id -> extruder it sits in (only valid when IN_EXTRUDER)
std::unordered_map<int, int> extruder_filament; // extruder_id -> currently loaded filament
// group_id -> filaments currently occupying that AMS channel (IN_SELECTOR or IN_EXTRUDER)
std::unordered_map<int, std::unordered_set<int>> ams_group_occupied;
filament_location.reserve(logical_filaments.size());
filament_extruder.reserve(logical_filaments.size());
// Initial state: every filament is in the AMS, every extruder is empty
for (int f : logical_filaments)
filament_location[f] = Location::IN_AMS;
// Slicer-estimate simulator: use NozzleStatusRecorder to track what each nozzle/extruder holds during slicing
NozzleStatusRecorder sliced_recorder;
const size_t seq_len = std::min(filament_change_seq.size(), nozzle_change_seq.size());
double actual_time = 0.0;
double sliced_time = 0.0;
for (size_t i = 0; i < seq_len; ++i) {
int B = filament_change_seq[i];
int nozzle_id = nozzle_change_seq[i];
auto nozzle_iter = nozzle_to_extruder.find(nozzle_id);
if (nozzle_iter == nozzle_to_extruder.end()) continue;
int E = nozzle_iter->second; // target extruder
// Step 0: compute the slicer-estimated time
// Slicer estimate: simulate the slicer's view (no selector awareness);
// count a load/unload when nozzle_in_extruder_change || filament_in_nozzle_change
if (calc_sliced_time) {
int old_nozzle_in_E = sliced_recorder.get_nozzle_in_extruder(E);
int old_filament_in_nozzle = sliced_recorder.get_filament_in_nozzle(nozzle_id);
int old_filament_in_ext = sliced_recorder.get_filament_in_nozzle(old_nozzle_in_E);
bool nozzle_change = (old_nozzle_in_E != nozzle_id);
bool filament_change = (old_filament_in_nozzle != B);
if (nozzle_change || filament_change) {
if (old_filament_in_ext != -1)
sliced_time += time_params.standard_unload_time;
sliced_time += time_params.standard_load_time;
}
sliced_recorder.set_nozzle_status(nozzle_id, B, E);
}
// Step 1: find the filament A currently loaded in the target extruder E
int A = -1;
{
auto it = extruder_filament.find(E);
if (it != extruder_filament.end())
A = it->second;
}
int group_B = get_group(B);
int group_A = (A != -1) ? get_group(A) : -1;
// Step 2: clear B's AMS-channel occupancy
auto group_it = ams_group_occupied.find(group_B);
if (group_it != ams_group_occupied.end()) {
for (int X : group_it->second) {
if (X == B) continue;
// X shares B's AMS channel, retreat it to the AMS to make way
Location loc_X = filament_location[X];
if (loc_X == Location::IN_EXTRUDER) {
actual_time += unload_ext_to_selector + unload_ams_to_selector;
int E2 = filament_extruder[X];
extruder_filament.erase(E2);
filament_extruder.erase(X);
} else if (loc_X == Location::IN_SELECTOR) {
actual_time += unload_ams_to_selector;
}
filament_location[X] = Location::IN_AMS;
}
group_it->second.clear();
}
// Step 3: A exits E (while A is still in the extruder)
// Step 3.5: pre-load B (in parallel with Step 3)
// actual time = max(Step 3, Step 3.5)
bool step3_executed = false;
float step3_time = 0.0f;
if (A != -1 && A != B && filament_location[A] == Location::IN_EXTRUDER) {
if (is_preload_enabled(group_A) && group_A != group_B) {
step3_time = unload_ext_to_selector;
filament_location[A] = Location::IN_SELECTOR;
} else {
step3_time = unload_ext_to_selector + unload_ams_to_selector;
filament_location[A] = Location::IN_AMS;
ams_group_occupied[group_A].erase(A);
}
extruder_filament.erase(E);
filament_extruder.erase(A);
step3_executed = true;
}
float step3_5_time = 0.0f;
if (step3_executed &&
filament_location[B] == Location::IN_AMS &&
group_A != group_B &&
is_preload_enabled(group_B)) {
step3_5_time = load_ams_to_selector;
filament_location[B] = Location::IN_SELECTOR;
ams_group_occupied[group_B].insert(B);
}
actual_time += std::max(step3_time, step3_5_time);
// Step 4: push B into E
// Step 6: pre-load the next filament C (in parallel with Step 4)
// actual time = max(Step 4, Step 6)
float step4_time = 0.0f;
Location loc_B = filament_location[B];
if (loc_B == Location::IN_AMS) {
step4_time = load_ams_to_selector + load_selector_to_ext;
} else if (loc_B == Location::IN_SELECTOR) {
step4_time = load_selector_to_ext;
}
// Step 5: update state
extruder_filament[E] = B;
filament_location[B] = Location::IN_EXTRUDER;
filament_extruder[B] = E;
ams_group_occupied[group_B].insert(B);
float step6_time = 0.0f;
if (i + 1 < seq_len) {
int C = filament_change_seq[i + 1];
int group_C = get_group(C);
if (filament_location[C] == Location::IN_AMS &&
group_C != group_B &&
is_preload_enabled(group_C) &&
ams_group_occupied[group_C].empty()) {
step6_time = load_ams_to_selector;
filament_location[C] = Location::IN_SELECTOR;
ams_group_occupied[group_C].insert(C);
}
}
actual_time += std::max(step4_time, step6_time);
}
result.actual_time = actual_time;
result.sliced_time = sliced_time;
return result;
}
// ==================== NozzleStatusRecorder implementation ====================
bool NozzleStatusRecorder::is_nozzle_empty(int nozzle_id) const
{
auto iter = nozzle_filament_status.find(nozzle_id);
if (iter == nozzle_filament_status.end()) return true;
return false;
}
int NozzleStatusRecorder::get_filament_in_nozzle(int nozzle_id) const
{
auto iter = nozzle_filament_status.find(nozzle_id);
if (iter == nozzle_filament_status.end()) return -1;
return iter->second;
}
int NozzleStatusRecorder::get_nozzle_in_extruder(int extruder_id) const
{
auto iter = extruder_nozzle_status.find(extruder_id);
if (iter == extruder_nozzle_status.end()) return -1;
return iter->second;
}
void NozzleStatusRecorder::set_nozzle_status(int nozzle_id, int filament_id, int extruder_id)
{
nozzle_filament_status[nozzle_id] = filament_id;
if (extruder_id != -1) {
extruder_nozzle_status[extruder_id] = nozzle_id;
}
}
void NozzleStatusRecorder::clear_nozzle_status(int nozzle_id)
{
auto iter = nozzle_filament_status.find(nozzle_id);
if (iter == nozzle_filament_status.end()) return;
nozzle_filament_status.erase(iter);
}
int LayeredNozzleGroupResult::estimate_seq_flush_weight(const std::vector<std::vector<std::vector<float>>>& flush_matrix, const std::vector<int>& filament_change_seq) const
{
auto get_weight_from_volume = [](float volume){
return static_cast<int>(volume * 1.26 * 0.01);
};
float total_flush_volume = 0;
NozzleStatusRecorder recorder;
for(auto filament: filament_change_seq){
auto nozzle = get_nozzle_for_filament(filament, -1);
if(!nozzle)
continue;
int extruder_id = nozzle->extruder_id;
int nozzle_id = nozzle->group_id;
int last_filament = recorder.get_filament_in_nozzle(nozzle_id);
if(last_filament!= -1 && last_filament != filament){
// bounds check to avoid out-of-range access
if (extruder_id >= 0 && extruder_id < static_cast<int>(flush_matrix.size()) &&
last_filament >= 0 && last_filament < static_cast<int>(flush_matrix[extruder_id].size()) &&
filament >= 0 && filament < static_cast<int>(flush_matrix[extruder_id][last_filament].size())) {
float flush_volume = flush_matrix[extruder_id][last_filament][filament];
total_flush_volume += flush_volume;
}
}
recorder.set_nozzle_status(nozzle_id, filament);
}
return get_weight_from_volume(total_flush_volume);
}
// ==================== StaticNozzleGroupResult ====================
std::optional<StaticNozzleGroupResult> StaticNozzleGroupResult::create(
const std::vector<FilamentInfo>& filaments_info,
const std::vector<NozzleInfo>& nozzles_info,
const std::vector<int>& filament_change_seq,
const std::vector<int>& nozzle_change_seq,
bool support_dynamic_nozzle_map)
{
if (filaments_info.empty() || nozzles_info.empty()) return std::nullopt;
std::map<int, NozzleInfo> nozzle_list_map;
std::map<int, std::set<int>> filament_to_nozzles;
for (auto nozzle_info : nozzles_info)
nozzle_list_map[nozzle_info.group_id] = nozzle_info;
for (auto filament_info : filaments_info) {
auto fil_id = filament_info.id;
auto nozzles_id = filament_info.group_id;
std::set<int> nozzles_set(nozzles_id.begin(), nozzles_id.end());
// Backward compat with older (single-nozzle) gcode.3mf: filament has no group_id, avoid an empty map.
if (nozzles_set.empty()) {
for (const auto& nozzle_entry : nozzle_list_map)
nozzles_set.insert(nozzle_entry.first);
}
filament_to_nozzles[fil_id] = nozzles_set;
}
StaticNozzleGroupResult result(support_dynamic_nozzle_map);
result._filament_to_nozzles = filament_to_nozzles;
result._nozzle_list_map = nozzle_list_map;
result._filament_change_seq = filament_change_seq;
result._nozzle_change_seq = nozzle_change_seq;
return result;
}
std::optional<NozzleInfo> StaticNozzleGroupResult::get_nozzle_from_id(int nozzle_id) const
{
auto iter = _nozzle_list_map.find(nozzle_id);
if (iter == _nozzle_list_map.end()) { return std::nullopt; }
return iter->second;
}
int StaticNozzleGroupResult::get_extruder_count() const
{
std::set<int> extruder_ids;
for (const auto &elem : _nozzle_list_map) { extruder_ids.insert(elem.second.extruder_id); }
return static_cast<int>(extruder_ids.size());
}
std::vector<NozzleInfo> StaticNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id) const
{
std::vector<NozzleInfo> result;
for (const auto &elem : _nozzle_list_map) {
const auto &nozzle = elem.second;
if (target_extruder_id == -1 || nozzle.extruder_id == target_extruder_id) {
result.push_back(nozzle);
}
}
return result;
}
std::vector<int> StaticNozzleGroupResult::get_used_extruders() const
{
std::set<int> used_extruders;
for (const auto &elem : _nozzle_list_map) { used_extruders.insert(elem.second.extruder_id); }
return std::vector<int>(used_extruders.begin(), used_extruders.end());
}
std::vector<unsigned int> StaticNozzleGroupResult::get_used_filaments() const
{
std::vector<unsigned int> used_filaments;
used_filaments.reserve(_filament_to_nozzles.size());
for (const auto &elem : _filament_to_nozzles) {
if (elem.first >= 0) {
used_filaments.push_back(static_cast<unsigned int>(elem.first));
}
}
return used_filaments;
}
std::vector<NozzleInfo> StaticNozzleGroupResult::get_nozzles_for_filament(int filament_id) const
{
auto iter = _filament_to_nozzles.find(filament_id);
if (iter == _filament_to_nozzles.end()) { return std::vector<NozzleInfo>(); }
std::vector<NozzleInfo> result;
for (int nozzle_id : iter->second) {
auto nozzle_iter = _nozzle_list_map.find(nozzle_id);
if (nozzle_iter != _nozzle_list_map.end()) {
result.push_back(nozzle_iter->second);
}
}
return result;
}
std::optional<NozzleInfo> StaticNozzleGroupResult::get_first_nozzle_for_filament(int filament_id) const
{
if (filament_id < 0) return std::nullopt;
if (!_filament_change_seq.empty() && _filament_change_seq.size() == _nozzle_change_seq.size()) {
for (size_t idx = 0; idx < _filament_change_seq.size(); ++idx) {
if (_filament_change_seq[idx] == filament_id) {
int nozzle_id = _nozzle_change_seq[idx];
auto nozzle = get_nozzle_from_id(nozzle_id);
if (nozzle) return nozzle;
}
}
}
auto iter = _filament_to_nozzles.find(filament_id);
if (iter == _filament_to_nozzles.end()) return std::nullopt;
for (int nozzle_id : iter->second) {
auto nozzle = get_nozzle_from_id(nozzle_id);
if (nozzle) return nozzle;
}
return std::nullopt;
}
// ==================== serialization ====================
std::string NozzleInfo::serialize() const
{
std::ostringstream oss;
oss << "id=\"" << group_id << "\" "
<< "extruder_id=\"" << extruder_id + 1 << "\" "
<< "nozzle_diameter=\"" << diameter << "\" "
<< "volume_type=\"" << get_nozzle_volume_type_string(volume_type) << "\"";
return oss.str();
}
std::string NozzleGroupInfo::serialize() const
{
std::ostringstream oss;
oss << extruder_id << "-"
<< std::setprecision(2) << diameter << "-"
<< get_nozzle_volume_type_string(volume_type) << "-"
<< nozzle_count;
return oss.str();
}
std::optional<NozzleGroupInfo> NozzleGroupInfo::deserialize(const std::string &str)
{
std::istringstream iss(str);
std::string token;
std::vector<std::string> tokens;
while (std::getline(iss, token, '-')) { tokens.push_back(token); }
if (tokens.size() != 4) { return std::nullopt; }
try {
int extruder_id = std::stoi(tokens[0]);
std::string diameter = tokens[1];
NozzleVolumeType volume_type = NozzleVolumeType(ConfigOptionEnum<NozzleVolumeType>::get_enum_values().at(tokens[2]));
int nozzle_count = std::stoi(tokens[3]);
return NozzleGroupInfo(diameter, volume_type, extruder_id, nozzle_count);
} catch (const std::exception &) {
return std::nullopt;
}
}
std::vector<NozzleInfo> load_nozzle_infos_with_compatibility(
const std::vector<NozzleInfo>& nozzle_infos,
const std::vector<FilamentInfo>& filament_infos,
const std::vector<int>& filament_map,
const std::vector<NozzleVolumeType>& extruder_volume_types,
const std::vector<double>& nozzle_diameter
)
{
bool has_nozzle_info = !nozzle_infos.empty();
bool has_valid_filament_info = !filament_infos.empty() && std::all_of(filament_infos.begin(), filament_infos.end(), [](const FilamentInfo& info){
return info.group_id.size() == 1;
});
if(!has_nozzle_info && !has_valid_filament_info){
BOOST_LOG_TRIVIAL(warning)<<__FUNCTION__ << ": building nozzle list from filament map and volume types";
// Backward compatibility for older gcode.3mf:
// - nozzle_diameter is always present and its size defines extruder count.
// - filament_map may be missing; treat it as [0, 0, ...] for each extruder.
// - extruder_volume_types may be missing; treat it as all Standard.
const size_t extruder_count = nozzle_diameter.size();
std::vector<NozzleVolumeType> volume_types_fixed = extruder_volume_types;
volume_types_fixed.resize(extruder_count, NozzleVolumeType::nvtStandard);
std::vector<NozzleInfo> result;
result.reserve(extruder_count);
for (size_t extruder_id = 0; extruder_id < extruder_count; ++extruder_id) {
NozzleInfo info;
info.diameter = format_diameter_to_str(nozzle_diameter[extruder_id]);
info.group_id = static_cast<int>(extruder_id);
info.extruder_id = static_cast<int>(extruder_id);
info.volume_type = volume_types_fixed[extruder_id];
result.emplace_back(std::move(info));
}
return result;
}
if(!has_nozzle_info){
BOOST_LOG_TRIVIAL(info)<<__FUNCTION__ << ": building nozzle list from filament info";
std::map<int, NozzleInfo> nozzle_map; // group_id -> NozzleInfo
for(auto& filament : filament_infos){
int group_id = filament.group_id.front();
if(group_id < 0 || nozzle_map.find(group_id) != nozzle_map.end()){
continue;
}
auto volume_type_str_to_enum = ConfigOptionEnum<NozzleVolumeType>::get_enum_values();
NozzleInfo info;
info.diameter = format_diameter_to_str(filament.nozzle_diameter);
info.group_id = group_id;
// Orca: bounds-check filament_map[filament.id] so a malformed 3mf (filament id
// beyond the map) degrades to extruder 0 instead of dereferencing out of range.
info.extruder_id = (filament.id >= 0 && filament.id < static_cast<int>(filament_map.size()))
? filament_map[filament.id] - 1
: 0; // to 0-based
if (volume_type_str_to_enum.count(filament.nozzle_volume_type))
info.volume_type = NozzleVolumeType(volume_type_str_to_enum.at(filament.nozzle_volume_type));
else {
info.volume_type = NozzleVolumeType::nvtStandard;
}
nozzle_map[group_id] = std::move(info);
}
std::vector<NozzleInfo> ret;
for(auto& elem : nozzle_map){
ret.emplace_back(elem.second);
}
return ret;
}
auto result = nozzle_infos;
std::sort(result.begin(), result.end());
BOOST_LOG_TRIVIAL(info)<<__FUNCTION__ << ": using new 3mf format with " << result.size() << " nozzle infos.";
return result;
}
}} // namespace Slic3r::MultiNozzleUtils
+296
View File
@@ -0,0 +1,296 @@
#ifndef MULTI_NOZZLE_UTILS_HPP
#define MULTI_NOZZLE_UTILS_HPP
#include <vector>
#include <map>
#include <optional>
#include <set>
#include <unordered_map>
#include "PrintConfig.hpp"
// Multi-nozzle support types.
// Declares the filament-grouping result types the slicing pipeline needs, plus the analytic
// filament-change-time model (FilamentChangeTimeParams, NozzleStatusRecorder,
// FilamentChangeSimResult, simulate_filament_change_time) — self-contained analytic code that
// never touches the time estimator; its first consumer is the filament_group golden harness.
// The gcode.3mf serialization surface lives here too: NozzleInfo/NozzleGroupInfo
// serialize+deserialize, the device-side StaticNozzleGroupResult,
// load_nozzle_infos_with_compatibility (the backward-compat 3mf reader) and
// LayeredNozzleGroupResult::estimate_seq_flush_weight. The change-time-tuning helpers
// calc_filament_change_gap_for_assignment / find_optimal_physical_assignment (used only by the
// AMS pre-load optimizer, a later feature) are not implemented here.
namespace Slic3r {
struct FilamentInfo; // Slic3r::FilamentInfo (ProjectTask.hpp) — consumed by StaticNozzleGroupResult / the 3mf reader
namespace MultiNozzleUtils {
// Information about a single logical nozzle.
struct NozzleInfo
{
std::string diameter;
NozzleVolumeType volume_type;
int extruder_id{-1}; // logical extruder id
int group_id{-1}; // logical nozzle id
std::string serialize() const;
bool operator<(const NozzleInfo& other) const {
if(group_id != other.group_id) return group_id < other.group_id;
if(extruder_id != other.extruder_id) return extruder_id < other.extruder_id;
if(volume_type != other.volume_type) return volume_type < other.volume_type;
return diameter < other.diameter;
}
};
// A group of identical nozzles on one extruder (diameter + volume type + count).
struct NozzleGroupInfo
{
std::string diameter;
NozzleVolumeType volume_type;
int extruder_id;
int nozzle_count;
NozzleGroupInfo() = default;
NozzleGroupInfo(const std::string& nozzle_diameter_, const NozzleVolumeType volume_type_, const int extruder_id_, const int nozzle_count_)
: diameter(nozzle_diameter_), volume_type(volume_type_), extruder_id(extruder_id_), nozzle_count(nozzle_count_)
{}
inline bool operator<(const NozzleGroupInfo &rhs) const
{
if (extruder_id != rhs.extruder_id) return extruder_id < rhs.extruder_id;
if (diameter != rhs.diameter) return diameter < rhs.diameter;
if (volume_type != rhs.volume_type) return volume_type < rhs.volume_type;
return nozzle_count < rhs.nozzle_count;
}
bool is_same_type(const NozzleGroupInfo &rhs) const
{
return diameter == rhs.diameter && volume_type == rhs.volume_type && extruder_id == rhs.extruder_id;
}
inline bool operator==(const NozzleGroupInfo &rhs) const
{
return diameter == rhs.diameter && volume_type == rhs.volume_type && extruder_id == rhs.extruder_id && nozzle_count == rhs.nozzle_count;
}
std::string serialize() const;
static std::optional<NozzleGroupInfo> deserialize(const std::string& str);
};
// Load/unload time constants used by the filament-change-time model.
// Consumed by simulate_filament_change_time() below and carried by the grouping-context
// substrate (FilamentGroupContext::SpeedInfo).
struct FilamentChangeTimeParams
{
float selector_load_time{0.0f};
float selector_unload_time{0.0f};
float standard_load_time{0.0f};
float standard_unload_time{0.0f};
};
/**
* @brief Abstract base for a nozzle-grouping result.
*/
class NozzleGroupResultBase
{
protected:
bool support_dynamic_nozzle_map{false}; // whether dynamic (selector) mapping is used
public:
NozzleGroupResultBase(bool support_dynamic_map = false) : support_dynamic_nozzle_map(support_dynamic_map) {}
virtual ~NozzleGroupResultBase() = default;
virtual std::optional<NozzleInfo> get_nozzle_from_id(int nozzle_id) const = 0;
virtual std::optional<NozzleInfo> get_first_nozzle_for_filament(int filament_id) const = 0; // logical nozzle a filament first uses
virtual std::vector<NozzleInfo> get_nozzles_for_filament(int filament_id) const = 0; // every nozzle a filament may use (across all layers)
bool is_support_dynamic_nozzle_map() const { return support_dynamic_nozzle_map; }
virtual int get_extruder_count() const = 0;
virtual std::vector<NozzleInfo> get_used_nozzles_in_extruder(int extruder_id =-1) const = 0;
virtual std::vector<int> get_used_extruders() const = 0;
virtual std::vector<unsigned int> get_used_filaments() const = 0;
};
/**
* @brief Layer-aware nozzle-grouping result.
* Used by the back-end slicing code; supports per-layer nozzle mapping.
*/
class LayeredNozzleGroupResult : public NozzleGroupResultBase
{
private:
std::vector<std::vector<int>> _layer_filament_nozzle_maps; // per-layer filament -> nozzle map
std::vector<std::vector<unsigned int>> _layer_filament_sequences; // per-layer filament print order
std::vector<int> _default_filament_nozzle_map; // global filament -> nozzle map
std::vector<unsigned int> _used_filaments; // all used filament indices
std::vector<NozzleInfo> _nozzle_list; // global nozzle list
public:
LayeredNozzleGroupResult(bool support_dynamic_map = false) : NozzleGroupResultBase(support_dynamic_map) {}
// No selector: one global filament->nozzle map.
static std::optional<LayeredNozzleGroupResult> create(
const std::vector<int>& filament_nozzle_map,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<unsigned int>& used_filaments);
// Selector: built from per-layer maps (each layer may differ).
static std::optional<LayeredNozzleGroupResult> create(
const std::vector<std::vector<int>>& layer_filament_nozzle_maps,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<unsigned int>& used_filaments,
const std::vector<std::vector<unsigned int>>& layer_filament_sequences);
// Multi-nozzle without selector: resolve each requested logical nozzle to a physical nozzle.
static std::optional<LayeredNozzleGroupResult> create(
const std::vector<unsigned int>& used_filaments,
const std::vector<int>& filament_map,
const std::vector<int>& filament_volume_map,
const std::vector<int>& filament_nozzle_map,
const std::vector<std::map<NozzleVolumeType, int>>& nozzle_count,
float diameter);
bool are_filaments_same_extruder(int filament_id1, int filament_id2, int layer_id = -1) const;
bool are_filaments_same_nozzle(int filament_id1, int filament_id2, int layer_id = -1) const;
int get_extruder_count() const override;
std::vector<NozzleInfo> get_used_nozzles_in_extruder(int target_extruder_id = -1) const override;
std::vector<NozzleInfo> get_used_nozzles_in_extruder(int target_extruder_id, int layer_id) const; // layer_id=-1 uses default map
std::vector<int> get_used_extruders() const override;
std::vector<int> get_used_extruders(int layer_id) const; // layer_id=-1 returns global extruders
std::vector<int> get_extruder_map(bool zero_based = true, int layer_id = -1) const;
std::vector<int> get_nozzle_map(int layer_id = -1) const;
std::vector<int> get_volume_map(int layer_id = -1) const;
std::vector<unsigned int> get_used_filaments() const override { return _used_filaments; }
std::vector<unsigned int> get_used_filaments(int layer_id) const;
std::optional<NozzleInfo> get_nozzle_for_filament(int filament_id, int layer_id = -1) const;
std::vector<NozzleInfo> get_nozzles_for_filament(int filament_id) const override;
std::optional<NozzleInfo> get_nozzle_from_id(int nozzle_id) const override;
std::optional<NozzleInfo> get_first_nozzle_for_filament(int filament_id) const override;
int get_extruder_id(int filament_id, int layer_id = -1) const;
int get_nozzle_id(int filament_id, int layer_id = -1) const;
size_t get_layer_count() const { return _layer_filament_nozzle_maps.size(); }
const std::vector<int>& get_layer_filament_nozzle_map(int layer_id) const;
const std::vector<std::vector<int>> &get_layer_filament_nozzle_maps() const { return _layer_filament_nozzle_maps; }
const std::vector<std::vector<unsigned int>>& get_layer_filament_sequences() const { return _layer_filament_sequences; }
// Estimate the flush weight of a filament-change sequence given the per-extruder flush matrix
// (extruder -> from-filament -> to-filament).
int estimate_seq_flush_weight(const std::vector<std::vector<std::vector<float>>>& flush_matrix, const std::vector<int>& filament_change_seq) const;
};
/**
* @brief Layer-less nozzle-grouping result for the device side (static nozzle mapping only).
* Reconstructed from a loaded gcode.3mf together with the filament/nozzle change sequences.
*/
class StaticNozzleGroupResult : public NozzleGroupResultBase
{
private:
std::map<int, std::set<int>> _filament_to_nozzles; // every nozzle a filament may map to
std::map<int, NozzleInfo> _nozzle_list_map; // used nozzles, keyed by logical nozzle id
std::vector<int> _filament_change_seq; // filament sequence used to resolve first-use
std::vector<int> _nozzle_change_seq; // logical-nozzle sequence paired with the filament sequence
public:
StaticNozzleGroupResult(bool support_dynamic_map) : NozzleGroupResultBase(support_dynamic_map) {}
// Build from a loaded 3mf, with the filament/nozzle change sequences.
static std::optional<StaticNozzleGroupResult> create(
const std::vector<FilamentInfo>& filaments_info,
const std::vector<NozzleInfo>& nozzles_info,
const std::vector<int>& filament_change_seq,
const std::vector<int>& nozzle_change_seq,
bool support_dynamic_map);
int get_extruder_count() const override;
std::vector<NozzleInfo> get_used_nozzles_in_extruder(int extruder_id = -1) const override;
std::vector<int> get_used_extruders() const override;
std::vector<unsigned int> get_used_filaments() const override;
std::optional<NozzleInfo> get_nozzle_from_id(int nozzle_id) const override;
std::vector<NozzleInfo> get_nozzles_for_filament(int filament_id) const override;
std::optional<NozzleInfo> get_first_nozzle_for_filament(int filament_id) const override;
};
// Tracks, during the filament-change simulation, which filament sits in each physical nozzle
// and which nozzle each extruder currently carries.
class NozzleStatusRecorder
{
private:
std::unordered_map<int, int> nozzle_filament_status; // Track filament in each nozzle
std::unordered_map<int, int> extruder_nozzle_status; // Track nozzle for each extruder
int current_extruder_id_ = -1; // Track current extruder id
public:
NozzleStatusRecorder() = default;
bool is_nozzle_empty(int nozzle_id) const;
int get_filament_in_nozzle(int nozzle_id) const;
int get_nozzle_in_extruder(int extruder_id) const;
int get_current_extruder_id() const { return current_extruder_id_; }
void clear_nozzle_status(int nozzle_id);
void set_current_extruder_id(int extruder_id) { current_extruder_id_ = extruder_id; }
// Update the status of a nozzle with new filament and extruder information
void set_nozzle_status(int nozzle_id, int filament_id, int extruder_id = -1);
// key: nozzle id, value: filament id (-1 = the nozzle carries no filament)
const std::unordered_map<int, int>& get_nozzle_filament_map() const { return nozzle_filament_status; }
// key: extruder id, value: nozzle id (-1 = the extruder carries no nozzle)
const std::unordered_map<int, int>& get_extruder_nozzle_map() const { return extruder_nozzle_status; }
};
struct FilamentChangeSimResult {
double actual_time = 0.0;
double sliced_time = 0.0;
};
// Analytic filament-change-time model. Given the used filaments, the nozzle
// list, the filament/nozzle change sequences, each filament's AMS group and the load/unload time
// constants, it simulates AMS->selector->extruder transport (with optional AMS pre-load overlap)
// and returns the actual print time plus the slicer-estimated time. Self-contained: it never
// touches the g-code time estimator.
FilamentChangeSimResult simulate_filament_change_time(
const std::vector<int>& logical_filaments,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<int>& filament_change_seq,
const std::vector<int>& nozzle_change_seq,
const std::vector<int>& group_of_filament,
const FilamentChangeTimeParams& time_params,
const std::vector<bool>& ams_preload_enabled = {},
bool calc_sliced_time = false);
// ==================== tool functions ====================
// Make each filament's per-layer nozzle assignment gap-free: layers where a filament is not
// extruded inherit the nozzle it last used (forward carry); layers before its first use inherit
// the first nozzle it ever uses (back-fill). Entries on layers where the filament is actually
// used stay untouched. Needed for stitched sequential maps, where consumers indexing with an
// object-local layer id must resolve the same nozzle as global-id consumers except across a
// genuine mid-print reassignment.
void normalize_nozzle_map_per_layer(std::vector<std::vector<int>>& layer_filament_nozzle_maps,
const std::vector<std::vector<unsigned int>>& layer_filaments);
std::vector<NozzleInfo> build_nozzle_list(std::vector<NozzleGroupInfo> info);
std::vector<NozzleInfo> build_nozzle_list(double diameter, const std::vector<int>& filament_nozzle_map,
const std::vector<int>& filament_volume_map, const std::vector<int>& filament_map);
// Load nozzle infos from a gcode.3mf, handling backward compatibility with older 3mf that did not
// record standalone <nozzle> tags: falls back to the per-filament group_id/diameter/volume_type, and
// (for the oldest single-nozzle 3mf) to the filament_map + extruder volume types + nozzle diameters.
std::vector<NozzleInfo> load_nozzle_infos_with_compatibility(
const std::vector<NozzleInfo>& nozzle_infos,
const std::vector<FilamentInfo>& filament_infos,
const std::vector<int>& filament_map,
const std::vector<NozzleVolumeType>& extruder_volume_types,
const std::vector<double>& nozzle_diameter
);
} // namespace MultiNozzleUtils
} // namespace Slic3r
#endif // MULTI_NOZZLE_UTILS_HPP
+13
View File
@@ -3,6 +3,7 @@
#include <cereal/access.hpp>
#include <cereal/types/base_class.hpp>
#include <cstddef>
namespace Slic3r {
@@ -41,6 +42,18 @@ private:
template<class Archive> void serialize(Archive &ar) { ar(id); }
};
struct ObjectInstanceID {
ObjectID object_id;
size_t instance_id { size_t(-1) };
bool operator==(const ObjectInstanceID& rhs) const { return object_id == rhs.object_id && instance_id == rhs.instance_id; }
bool operator!=(const ObjectInstanceID& rhs) const { return !(*this == rhs); }
bool operator<(const ObjectInstanceID& rhs) const
{
return object_id < rhs.object_id || (object_id == rhs.object_id && instance_id < rhs.instance_id);
}
};
// Base for Model, ModelObject, ModelVolume, ModelInstance or ModelMaterial to provide a unique ID
// to synchronize the front end (UI) with the back end (BackgroundSlicingProcess / Print / PrintObject).
// Also base for Print, PrintObject, SLAPrint, SLAPrintObject to provide a unique ID for matching Model / ModelObject
+192 -15
View File
@@ -27,19 +27,20 @@ namespace Slic3r {
namespace orientation {
struct CostItems {
float overhang;
float bottom;
float bottom_hull;
float contour;
float area_laf; // area_of_low_angle_faces
float area_projected; // area of projected 2D profile
float volume;
float area_total; // total area of all faces
float radius; // radius of bounding box
float height_to_bottom_hull_ratio; // affects stability, the lower the better
float unprintability;
float overhang = 0;
float bottom = 0;
float bottom_hull = 0;
float contour = 0;
float area_laf = 0; // area_of_low_angle_faces
float area_projected = 0; // area of projected 2D profile
float volume = 0;
float area_total = 0; // total area of all faces
float radius = 0; // radius of bounding box
float height_to_bottom_hull_ratio = 0; // affects stability, the lower the better
float unprintability = 0;
Eigen::VectorXf areas_cooling;
CostItems(CostItems const & other) = default;
CostItems() { memset(this, 0, sizeof(*this)); }
CostItems() = default;
static std::string field_names() {
return " overhang, bottom, bothull, contour, A_laf, A_prj, unprintability";
}
@@ -68,10 +69,11 @@ public:
Eigen::VectorXf z_max, z_max_hull; // max of projected z
Eigen::VectorXf z_median; // median of projected z
Eigen::VectorXf z_mean; // mean of projected z
Eigen::VectorXf areas_cooling; // weighted areas for cool direction
std::vector<Vec3f> face_normals;
std::vector<Vec3f> face_normals_hull;
OrientParams params;
bool has_cooling_fan = false;
std::vector< Vec3f> orientations; // Vec3f == stl_normal
std::function<void(unsigned)> progressind = { }; // default empty indicator function
@@ -85,6 +87,7 @@ public:
orient_mesh = orient_mesh_;
mesh = &orient_mesh->mesh;
params = params_;
has_cooling_fan = orient_mesh->has_cooling_fan;
progressind = progressind_;
params.ASCENT = cos(PI - orient_mesh->overhang_angle * PI / 180); // use per-object overhang angle
@@ -158,12 +161,14 @@ public:
//To avoid flipping, we need to verify if there are orientations with same unprintability.
Vec3f n1 = {0, 0, 1};
auto best_orientation = results_vector[0].first;
size_t best_index = 0;
for (int i = 1; i< results_vector.size()-1; i++) {
if (abs(results_vector[i].second.unprintability - results_vector[0].second.unprintability) < EPSILON && abs(results_vector[0].first.dot(n1)-1) > EPSILON) {
if (abs(results_vector[i].first.dot(n1)-1) < EPSILON*EPSILON) {
if (abs(results_vector[i].first.dot(n1)-1) < EPSILON*EPSILON) {
best_orientation = n1;
break;
best_index = i;
break;
}
}
else {
@@ -172,6 +177,9 @@ public:
}
// cooling weights are per-orientation, so take them from the orientation actually chosen
areas_cooling = results_vector[best_index].second.areas_cooling;
BOOST_LOG_TRIVIAL(info) << std::fixed << std::setprecision(6) << "best:" << best_orientation.transpose() << ", costs:" << results_vector[0].second.field_values();
std::cout << std::fixed << std::setprecision(6) << "best:" << best_orientation.transpose() << ", costs:" << results_vector[0].second.field_values() << std::endl;
@@ -441,6 +449,19 @@ public:
Eigen::MatrixXf laf_areas = ((normal_projection_abs.array() < params.LAF_MAX) * (normal_projection_abs.array() > params.LAF_MIN) * (z_max.array() > total_min_z + params.FIRST_LAY_H)).select(areas, 0);
costs.area_laf = laf_areas.sum();
if (has_cooling_fan)
{
// Angle range of overhang faces requiring cooling
float angle_thres_high = -0.6427f;
float angle_thres_low = -0.97f;
// compute the weighted overhang faces area
Eigen::VectorXf ones_f = Eigen::VectorXf::Ones(mesh->facets_count());
auto overhang_area_condition = (normal_projection.array() < angle_thres_high && normal_projection.array() > angle_thres_low).eval();
Eigen::VectorXf areas_ = (overhang_area_condition * !bottom_condition_2nd).select(areas, 0);
Eigen::VectorXf weighted_areas = areas_.cwiseProduct(ones_f - normal_projection);
costs.areas_cooling = weighted_areas;
}
// height to bottom_hull_area ratio
//float total_max_z = z_projected.maxCoeff();
//costs.height_to_bottom_hull_ratio = SQ(total_max_z) / (costs.bottom_hull + 1e-7);
@@ -468,6 +489,67 @@ public:
return cost;
}
Vec3d find_cooling_direction2(Vec3d euler_angles, const Eigen::VectorXf& areas_in, TriangleMesh& mesh)
{
Vec3f machine_cool_dir = this->orient_mesh->cooling_direction.cast<float>();
const size_t num_faces = areas.rows();
Vec3f best_direction = { 0, 0, 0 };
// 1. Make a copy of input mesh, rotate and translate to the best orientation
TriangleMesh mesh_copy = TriangleMesh(mesh.its);
mesh_copy.rotate_x(euler_angles(0, 0));
mesh_copy.rotate_y(euler_angles(1, 0));
mesh_copy.rotate_z(euler_angles(2, 0));
auto bounding_box = mesh_copy.bounding_box();
Eigen::VectorXf translate_distance = bounding_box.min.array().cast<float>();
Vec3d mesh_center = mesh_copy.center();
mesh_copy.translate(-mesh_center(0), -mesh_center(1), -translate_distance(2));
// 2. sample cooling direction
const size_t sample_nums = 180;
std::vector<Vec3f> cool_dirs;
for (size_t i = 0; i < sample_nums; i++)
{
float angle_deg = i * (360.0 / sample_nums);
float angle_rad = angle_deg * (PI / 180.0);
cool_dirs.push_back(Vec3f{ std::cos(angle_rad), std::sin(angle_rad), 0});
}
// 3. accumulate the weighted projected overhang area, find the max weighted project area direction
std::vector<Vec3f> face_normals_copy = its_face_normals(mesh_copy.its);
float overhang_projected_max = 0.f;
float overhang_projected_origin = 0.f;
for (auto cool_dir : cool_dirs)
{
float overhang_projected_tmp = 0.f;
for (size_t i = 0; i < num_faces; i++)
{
float cool_dir_projection = face_normals_copy[i].dot(cool_dir);
if (areas_in[i] > 0 && cool_dir_projection > 0)
{
overhang_projected_tmp += areas_in[i] * cool_dir_projection;
}
}
if (overhang_projected_tmp > overhang_projected_max)
{
overhang_projected_max = overhang_projected_tmp;
best_direction = cool_dir;
}
if (cool_dir.dot(machine_cool_dir) > 0.999)
{
overhang_projected_origin = overhang_projected_tmp;
}
}
// The symmetric model has similar overhang projection at all angles, so Z-axis rotation is unnecessary.
if (std::abs(overhang_projected_origin - overhang_projected_max) < 1.0f)
{
best_direction = machine_cool_dir;
}
BOOST_LOG_TRIVIAL(info) << "best cooling dir = " << best_direction.transpose() << "\n";
return best_direction.cast<double>();
}
};
void _orient(OrientMeshs& meshs_,
@@ -497,6 +579,13 @@ void _orient(OrientMeshs& meshs_,
mesh_.orientation = orienter.process();
Geometry::rotation_from_two_vectors(mesh_.orientation, { 0,0,1 }, mesh_.axis, mesh_.angle, &mesh_.rotation_matrix);
mesh_.euler_angles = Geometry::extract_euler_angles(mesh_.rotation_matrix);
// find cool direction
if (mesh_.has_cooling_fan)
{
mesh_.orientation_vertical = orienter.find_cooling_direction2(mesh_.euler_angles, orienter.areas_cooling, mesh_.mesh);
BOOST_LOG_TRIVIAL(info) << "cooling direction: " << mesh_.orientation_vertical.transpose() << "\n";
Geometry::rotation_from_two_vectors(mesh_.orientation_vertical, mesh_.cooling_direction, mesh_.axis_vertical, mesh_.angle_vertical, &mesh_.rotation_matrix_vertical);
}
BOOST_LOG_TRIVIAL(debug) << "rotation_from_two_vectors: " << mesh_.orientation << "; " << mesh_.axis << "; " << mesh_.angle << "; euler: " << mesh_.euler_angles.transpose();
}});
}
@@ -539,6 +628,94 @@ void orient(ModelInstance* instance)
instance->rotate(rotation_matrix);
}
void orient_for_cooling(TriangleMesh& mesh, const FanDirection& fan_dir)
{
Vec3f best_direction{ 0, 0, 0 };
Vec3f machine_cool_dir{ 0, 0, 0 };
if (fan_dir == FanDirection::fdUndefine)
{
// no cooling fan, do not rotate along z axis
return;
}
else if (fan_dir == FanDirection::fdRight)
{
machine_cool_dir = { 1, 0, 0 }; // the cooling fan is on the right side.
}
else
{
// the cooling fan is on the left side or both side has cooling fans
machine_cool_dir = { -1, 0, 0 };
}
// 1. filter the overhang_areas
int nfaces = mesh.facets_count();
auto face_normals = its_face_normals(mesh.its);
Eigen::VectorXf normal_projection(nfaces, 1);
for (auto i = 0; i < nfaces; i++)
{
normal_projection(i) = face_normals[i].dot(Vec3f(0, 0, 1));
}
float angle_thres_high = -0.6427f;
float angle_thres_low = -0.97f;
// 2. compute the weighted overhang faces area
Eigen::VectorXf weighted_areas = Eigen::VectorXf::Zero(nfaces);
for (int i = 0; i < nfaces; i++)
{
if (normal_projection(i) < angle_thres_high && normal_projection(i) > angle_thres_low)
{
weighted_areas(i) = mesh.its.facet_area(i) * (1.0f - normal_projection(i));
}
}
const size_t sample_nums = 180;
std::vector<Vec3f> cool_dirs;
for (size_t i = 0; i < sample_nums; i++)
{
float angle_deg = i * (360.0 / sample_nums);
float angle_rad = angle_deg * (PI / 180.0);
cool_dirs.push_back(Vec3f{ std::cos(angle_rad), std::sin(angle_rad), 0 });
}
// 3. accumulate the weighted projected overhang area, find the max weighted project area direction
float overhang_projected_max = 0.f;
float overhang_projected_origin = 0.f;
for (auto cool_dir : cool_dirs)
{
float overhang_projected_tmp = 0.f;
for (size_t i = 0; i < nfaces; i++)
{
float cool_dir_projection = face_normals[i].dot(cool_dir);
if (weighted_areas[i] > 0 && cool_dir_projection > 0)
{
overhang_projected_tmp += weighted_areas[i] * cool_dir_projection;
}
}
if (overhang_projected_tmp > overhang_projected_max)
{
overhang_projected_max = overhang_projected_tmp;
best_direction = cool_dir;
}
if (cool_dir.dot(machine_cool_dir) > 0.999)
{
overhang_projected_origin = overhang_projected_tmp;
}
}
// The symmetric model has similar overhang projection at all angles, so Z-axis rotation is unnecessary.
if (std::abs(overhang_projected_origin - overhang_projected_max) < 1.0f)
{
return;
}
// rotate the mesh
Vec3d axis;
double angle;
Matrix3d rotation_matrix;
Geometry::rotation_from_two_vectors(best_direction.cast<double>(), machine_cool_dir.cast<double>(), axis, angle, &rotation_matrix);
mesh.rotate(angle, axis);
}
} // namespace arr
} // namespace Slic3r
+48 -87
View File
@@ -26,10 +26,18 @@ struct OrientMesh {
TriangleMesh mesh; /// The real mesh data
double overhang_angle = 30;
double angle{ 0 };
double angle_vertical{ 0 };
Vec3d axis{ 0,0,1 };
Vec3d axis_vertical{ 0,0,1 };
Vec3d orientation{ 0,0,1 };
Matrix3d rotation_matrix;
Vec3d euler_angles;
Vec3d orientation_vertical{ -1,0,0 };
Matrix3d rotation_matrix = Matrix3d::Identity();
Matrix3d rotation_matrix_vertical = Matrix3d::Identity();
Vec3d euler_angles = {0, 0, 0};
Vec3d euler_angles_vertical = {0, 0, 0};
Vec3d cooling_direction = {0, 0, 0};
bool has_cooling_fan{false};
std::string name;
/// Optional setter function which can store arbitrary data in its closure
@@ -40,100 +48,50 @@ struct OrientMesh {
};
// params for minimizing support area
struct OrientParamsArea {
float TAR_A = 0.015f;
float TAR_B = 0.177f;
float RELATIVE_F = 20;
float CONTOUR_F = 0.5f;
float BOTTOM_F = 2.5f;
float BOTTOM_HULL_F = 0.1f;
float TAR_C = 0.1f;
float TAR_D = 1;
float TAR_E = 0.0115f;
float FIRST_LAY_H = 0.2f;//0.0475;
float VECTOR_TOL = -0.00083f;
float NEGL_FACE_SIZE = 0.01f;
float ASCENT = -0.5f;
float PLAFOND_ADV = 0.0599f;
float CONTOUR_AMOUNT = 0.0182427f;
float OV_H = 2.574f;
float height_offset = 2.3728f;
float height_log = 0.041375f;
float height_log_k = 1.9325457f;
float LAF_MAX = 0.999f; // cos(1.4\degree) for low angle face 0.9997f
float LAF_MIN = 0.97f; // cos(14\degree) 0.9703f
float TAR_LAF = 0.001f; //0.01f
float TAR_PROJ_AREA = 0.1f;
float BOTTOM_MIN = 0.1f; // min bottom area. If lower than it the object may be unstable
float BOTTOM_MAX = 2000; // max bottom area. If get to it the object is stable enough (further increase bottom area won't do more help)
float height_to_bottom_hull_ratio_MIN = 1;
float BOTTOM_HULL_MAX = 2000;// max bottom hull area
float APPERANCE_FACE_SUPP=3; // penalty of generating supports on appearance face
float overhang_angle = 60.f;
bool use_low_angle_face = true;
bool min_volume = false;
Eigen::Vector3f fun_dir;
/// Allow parallel execution.
bool parallel = true;
/// Progress indicator callback called when an object gets packed.
/// The unsigned argument is the number of items remaining to pack.
std::function<void(unsigned, std::string)> progressind = {};
/// A predicate returning true if abort is needed.
std::function<bool(void)> stopcondition = {};
OrientParamsArea() = default;
};
struct OrientParams {
float TAR_A = 0.01f;//0.128f;
float TAR_B = 0.177f;
float RELATIVE_F= 6.610621027964314f;
float CONTOUR_F = 0.23228623269775997f;
float BOTTOM_F = 1.167152017941474f;
float BOTTOM_HULL_F = 0.1f;
float TAR_C = 0.24308070476924726f;
float TAR_D = 0.6284515508160871f;
float TAR_E = 0;//0.032157292647062234;
float FIRST_LAY_H = 0.2f;//0.029;
float VECTOR_TOL = -0.0011163303070972383f;
float NEGL_FACE_SIZE = 0.1f;
float ASCENT= -0.5f;
float PLAFOND_ADV = 0.04079208948120519f;
float CONTOUR_AMOUNT = 0.0101472219892684f;
float OV_H = 1.0370178217794535f;
float height_offset = 2.7417608343142073f;
float height_log = 0.06442030687034085f;
float height_log_k = 0.3933594673063997f;
float LAF_MAX = 0.999f; // cos(1.4\degree) for low angle face //0.9997f;
float LAF_MIN= 0.9703f; // cos(14\degree) 0.9703f;
float TAR_LAF = 0.01f; //0.1f
float TAR_PROJ_AREA = 0.1f;
float BOTTOM_MIN = 0.1f; // min bottom area. If lower than it the objects may be unstable
float BOTTOM_MAX = 2000; //400
float height_to_bottom_hull_ratio_MIN = 1;
float BOTTOM_HULL_MAX = 2000;// max bottom hull area to clip //600
float APPERANCE_FACE_SUPP=3; // penalty of generating supports on appearance face
float overhang_angle = 60.f;
bool use_low_angle_face = true;
bool min_volume = false;
Eigen::Vector3f fun_dir;
float TAR_A { 0.01f }; // 0.128f;
float TAR_B { 0.177f };
float RELATIVE_F { 6.610621027964314f };
float CONTOUR_F { 0.23228623269775997f };
float BOTTOM_F { 1.167152017941474f };
float BOTTOM_HULL_F { 0.1f };
float TAR_C { 0.24308070476924726f };
float TAR_D { 0.6284515508160871f };
float TAR_E { 0}; // 0.032157292647062234;
float FIRST_LAY_H { 0.2f}; // 0.029;
float VECTOR_TOL { -0.0011163303070972383f };
float NEGL_FACE_SIZE { 0.1f };
float ASCENT { -0.5f };
float PLAFOND_ADV { 0.04079208948120519f };
float CONTOUR_AMOUNT { 0.0101472219892684f };
float OV_H { 1.0370178217794535f };
float height_offset { 2.7417608343142073f };
float height_log { 0.06442030687034085f };
float height_log_k { 0.3933594673063997f };
float LAF_MAX { 0.999f }; // cos(1.4\degree) for low angle face //0.9997f;
float LAF_MIN { 0.9703f }; // cos(14\degree) 0.9703f;
float TAR_LAF { 0.01f }; // 0.1f
float TAR_PROJ_AREA { 0.1f };
float BOTTOM_MIN { 0.1f }; // min bottom area. If lower than it the objects may be unstable
float BOTTOM_MAX { 2000 }; // 400
float height_to_bottom_hull_ratio_MIN { 1 };
float BOTTOM_HULL_MAX { 2000 }; // max bottom hull area to clip //600
float APPERANCE_FACE_SUPP { 3 }; // penalty of generating supports on appearance face
float overhang_angle { 60.f };
bool use_low_angle_face { true };
bool min_volume { false };
Eigen::Vector3f fun_dir {};
/// Allow parallel execution.
bool parallel = false;
bool parallel { false };
/// Progress indicator callback called when an object gets packed.
/// The unsigned argument is the number of items remaining to pack.
std::function<void(unsigned, std::string)> progressind = {};
std::function<void(unsigned, std::string)> progressind {};
/// A predicate returning true if abort is needed.
std::function<bool(void)> stopcondition = {};
std::function<bool(void)> stopcondition {};
OrientParams() = default;
};
@@ -154,6 +112,9 @@ void orient(ModelObject* obj);
void orient(ModelInstance* instance);
// rotate z axis for cooling
void orient_for_cooling(TriangleMesh& mesh, const FanDirection& fan_dir);
}} // namespace Slic3r::orientment
#endif // MODELORIENT_HPP
+287 -34
View File
@@ -360,6 +360,14 @@ static ClipperLib_Z::Paths clip_extrusion(const ClipperLib_Z::Path& subject, con
return clipped_paths;
}
static double clipper_z_path_length(const ClipperLib_Z::Path &path)
{
double len = 0.;
for (size_t i = 1; i < path.size(); ++ i)
len += (Vec2d(double(path[i].x()), double(path[i].y())) - Vec2d(double(path[i - 1].x()), double(path[i - 1].y()))).norm();
return len;
}
struct PerimeterGeneratorArachneExtrusion
{
Arachne::ExtrusionLine* extrusion = nullptr;
@@ -571,6 +579,158 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
return extrusion_coll;
}
// ORCA: only_one_wall_top acts on top surfaces, so without a top shell there is nothing for it to act on: zero top
// shell layers retype the top surfaces as internal, see LayerRegion::prepare_fill_surfaces(). A 0% top surface
// density does leave a top surface - just an unfilled one - so it does not disable the feature.
// ConfigManipulation::toggle_print_fff_options() hides the option under the same condition, so a profile that left
// it enabled does not act behind a hidden checkbox.
static bool has_top_shell_layers(const PrintRegionConfig &config)
{
return config.top_shell_layers.value > 0;
}
// ORCA: only_one_wall_first_layer thins the first layer to a single wall, the bottom counterpart of the above and
// gated the same way: zero bottom shell layers retype the bottom surfaces as internal, so that wall would ring
// sparse infill on the bed. The bottom surface density plays no part - an unfilled bottom surface is still a bottom
// surface, exactly as for the top - and it cannot reach zero anyway, being capped at a 10% minimum.
static bool has_bottom_shell_layers(const PrintRegionConfig &config)
{
return config.bottom_shell_layers.value > 0;
}
// ORCA: the inner walls are only given up when a top fill takes their space, and it has to actually reach it -
// a 0% top surface density leaves no fill at all, and without top_surface_expansion the fill never grows over
// them. Either way the original generation is kept (re-onion the not-top region), which is what users of
// only_one_wall_top alone have always got.
static bool top_fill_replaces_inner_walls(const PrintRegionConfig &config)
{
return has_top_shell_layers(config) && config.top_surface_density.value > 0 && config.top_surface_expansion.value > 0;
}
// ORCA: only_one_wall_top - cheap per-vertex classification of a wall against the top surface. Only Partial
// needs the geometry clipped or measured; a segment crossing the top with no vertex inside is rare enough to ignore.
enum class TopOverlap { None, Partial, Full };
static bool point_over_top(const Point &p, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
{
if (! top_region_bbox.contains(p))
return false;
for (const ExPolygon &ex : top_region)
if (ex.contains(p, false))
return true;
return false;
}
static TopOverlap classify_over_top(const Points &pts, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
{
size_t inside = 0;
for (const Point &p : pts)
if (point_over_top(p, top_region, top_region_bbox))
++ inside;
return inside == 0 ? TopOverlap::None : inside == pts.size() ? TopOverlap::Full : TopOverlap::Partial;
}
static TopOverlap classify_over_top(const Arachne::ExtrusionLine &el, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
{
size_t inside = 0;
for (const Arachne::ExtrusionJunction &j : el.junctions)
if (point_over_top(j.p, top_region, top_region_bbox))
++ inside;
return inside == 0 ? TopOverlap::None : inside == el.junctions.size() ? TopOverlap::Full : TopOverlap::Partial;
}
// ORCA: only_one_wall_top for Arachne - cut out of the already generated inner walls the parts running over the top
// surface, so geometry that continues upward keeps its walls. A wall too short over the top to be worth slitting open
// is left whole, its footprint reported in kept_over_top for the caller to withhold from the top fill.
static void clip_inner_walls_over_top(std::vector<Arachne::VariableWidthLines> &inner_perimeters, const ExPolygons &top_region, coord_t perimeter_width, Polygons &kept_over_top)
{
const BoundingBox top_region_bbox = get_extents(top_region).inflated(SCALED_EPSILON);
auto covered_by = [](const Arachne::ExtrusionLine &el) {
Polyline centerline;
centerline.points.reserve(el.junctions.size());
coord_t width = 0;
for (const Arachne::ExtrusionJunction &j : el.junctions) {
centerline.points.emplace_back(j.p);
width = std::max(width, j.w);
}
return offset(centerline, float(width) / 2.f);
};
// Pull the cut back by half a wall width: the clip severs the centerline, but the bead's rounded end
// extends half a width past its endpoint and would otherwise overlap the top fill.
ClipperLib_Z::Paths top_paths_z;
for (const Polygon &poly : to_polygons(offset_ex(top_region, float(perimeter_width) / 2.f))) {
top_paths_z.emplace_back();
ClipperLib_Z::Path &out = top_paths_z.back();
out.reserve(poly.points.size());
for (const Point &pt : poly.points)
out.emplace_back(pt.x(), pt.y(), 0);
}
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) {
Arachne::VariableWidthLines kept;
kept.reserve(inner_perimeter.size());
for (Arachne::ExtrusionLine &el : inner_perimeter) {
if (el.empty())
continue;
const TopOverlap overlap = classify_over_top(el, top_region, top_region_bbox);
if (overlap == TopOverlap::None) {
kept.emplace_back(std::move(el));
continue;
}
if (overlap == TopOverlap::Full)
continue; // the clip below would return nothing anyway
ClipperLib_Z::Path subject;
subject.reserve(el.size());
for (const Arachne::ExtrusionJunction &j : el.junctions)
subject.emplace_back(j.p.x(), j.p.y(), j.w);
ClipperLib_Z::Paths pieces = clip_extrusion(subject, top_paths_z, ClipperLib_Z::ctDifference);
// Clipper treats the subject as an open polyline, so it also cuts a closed loop at its (arbitrary)
// start vertex and may reverse pieces. Stitch pieces sharing an endpoint back together.
auto same_pt = [](const ClipperLib_Z::IntPoint &p, const ClipperLib_Z::IntPoint &q) {
return std::abs(p.x() - q.x()) <= SCALED_EPSILON && std::abs(p.y() - q.y()) <= SCALED_EPSILON;
};
for (size_t i = 0; i < pieces.size(); ++ i) {
for (size_t j = i + 1; j < pieces.size();) {
ClipperLib_Z::Path &a = pieces[i];
ClipperLib_Z::Path &b = pieces[j];
if (same_pt(a.front(), b.front()) || same_pt(a.front(), b.back()))
std::reverse(a.begin(), a.end());
if (same_pt(a.back(), b.back()))
std::reverse(b.begin(), b.end());
if (same_pt(a.back(), b.front())) {
a.insert(a.end(), b.begin() + 1, b.end());
pieces.erase(pieces.begin() + j);
j = i + 1; // the merged path has new endpoints, restart the scan
} else
++ j;
}
}
// If the clip removed next to nothing, keep the loop untouched instead of slitting it open. The
// half-width pull-back above already costs about one width per crossing, hence two widths.
double kept_length = 0.;
for (const ClipperLib_Z::Path &path : pieces)
kept_length += clipper_z_path_length(path);
if (clipper_z_path_length(subject) - kept_length < 2. * double(perimeter_width)) {
append(kept_over_top, covered_by(el));
kept.emplace_back(std::move(el));
continue;
}
for (const ClipperLib_Z::Path &path : pieces) {
Arachne::ExtrusionLine clipped(el.inset_idx, el.is_odd);
clipped.junctions.reserve(path.size());
for (const ClipperLib_Z::IntPoint &pt : path)
clipped.junctions.emplace_back(Point(pt.x(), pt.y()), coord_t(pt.z()), el.inset_idx);
// Discard tiny leftovers that would print as zits.
if (clipped.size() >= 2 && clipped.getLength() >= perimeter_width)
kept.emplace_back(std::move(clipped));
}
}
inner_perimeter = std::move(kept);
}
}
void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExPolygons &top_fills,
ExPolygons &non_top_polygons, ExPolygons &fill_clip) const {
// other perimeters
@@ -636,6 +796,7 @@ void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExP
ExPolygons delete_bridge = diff_ex(orig_polygons, bridge_checker, ApplySafetyOffset::Yes);
ExPolygons top_polygons = diff_ex(delete_bridge, upper_polygons_series_clipped, ApplySafetyOffset::Yes);
// get the not-top surface, from the "real top" but enlarged by external_infill_margin (and the
// min_width_top_surface we removed a bit before)
ExPolygons temp_gap = diff_ex(top_polygons, fill_clip);
@@ -1219,6 +1380,11 @@ void PerimeterGenerator::process_classic()
for (const Surface &surface : all_surfaces)
surface_exp.push_back(surface.expolygon);
std::vector<size_t> surface_order = chain_expolygons(surface_exp);
// ORCA: neither one-wall option has a surface to act on without the shell behind it, see
// has_top_shell_layers() / has_bottom_shell_layers(). Gated here so every use below - including the
// topmost and first layers - sees the same answer.
const bool only_one_wall_top = this->config->only_one_wall_top && has_top_shell_layers(*this->config);
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
for (size_t order_idx = 0; order_idx < surface_order.size(); order_idx++) {
const Surface &surface = all_surfaces[surface_order[order_idx]];
// detect how many perimeters must be generated for this island
@@ -1226,16 +1392,23 @@ void PerimeterGenerator::process_classic()
int sparse_infill_density = this->config->sparse_infill_density.value;
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
loop_number++;
if (this->layer_id == object_config->raft_layers && this->config->only_one_wall_first_layer)
if (this->layer_id == object_config->raft_layers && only_one_wall_first_layer)
loop_number = 0;
// Set the topmost layer to be one wall
if (loop_number > 0 && config->only_one_wall_top && this->upper_slices == nullptr)
if (loop_number > 0 && only_one_wall_top && this->upper_slices == nullptr)
loop_number = 0;
ExPolygons last = union_ex(surface.expolygon.simplify_p(surface_simplify_resolution));
ExPolygons gaps;
ExPolygons top_fills;
ExPolygons fill_clip;
// ORCA: only_one_wall_top, all empty unless this island has a top surface on this layer. See the
// post-onion reduction below: the region to keep clear of inner walls, the space freed by the dropped
// walls (goes to infill, not left as a void) and the space held by the kept ones (withheld from the fill).
ExPolygons one_wall_top_region;
ExPolygons one_wall_top_reclaimed;
Polygons one_wall_top_kept_bands;
bool apply_one_wall_top = false;
if (loop_number >= 0) {
// In case no perimeters are to be generated, loop_number will equal to -1.
std::vector<PerimeterGeneratorLoops> contours(loop_number+1); // depth => loops
@@ -1374,8 +1547,19 @@ void PerimeterGenerator::process_classic()
//BBS: refer to superslicer
//store surface for top infill if only_one_wall_top
if (i == 0 && i!=loop_number && config->only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) {
this->split_top_surfaces(last, top_fills, last, fill_clip);
if (i == 0 && i!=loop_number && only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) {
if (top_fill_replaces_inner_walls(*this->config)) {
// ORCA: take the top fill and the keep-out region but leave `last` as the real geometry,
// so the onion follows it and the walls over the top are reduced in one step below.
ExPolygons non_top_polygons;
this->split_top_surfaces(last, top_fills, non_top_polygons, fill_clip);
apply_one_wall_top = !top_fills.empty();
if (apply_one_wall_top)
one_wall_top_region = diff_ex(last, non_top_polygons);
} else {
// Onion the not-top region only, so the remaining walls stop at the top boundary.
this->split_top_surfaces(last, top_fills, last, fill_clip);
}
}
if (i == loop_number && (! has_gap_fill || this->config->sparse_infill_density.value == 0)) {
@@ -1385,6 +1569,46 @@ void PerimeterGenerator::process_classic()
}
}
// ORCA: only_one_wall_top reduction - drop the inner walls (depth > 0) running over the top surface and
// take that space back from the gaps, leaving the top with the outer wall and the top infill. Classic
// perimeters are closed loops, so a wall can only be kept or dropped whole; one that merely grazes the
// top (same tolerance as the Arachne clip) is kept and withheld from the top fill instead.
if (apply_one_wall_top) {
const BoundingBox top_region_bbox = get_extents(one_wall_top_region).inflated(SCALED_EPSILON);
const double grazing_tolerance = 2. * double(perimeter_width);
// The band a wall covers, taken around its centerline so the orientation of holes does not matter.
auto wall_band = [perimeter_spacing](const Polygon &poly) {
Polygon centerline = poly;
centerline.make_counter_clockwise();
return diff(offset(centerline, float(perimeter_spacing) / 2.f),
offset(centerline, -float(perimeter_spacing) / 2.f));
};
Polygons dropped_wall_bands;
auto reduce_over_top = [&](PerimeterGeneratorLoops &loops) {
loops.erase(std::remove_if(loops.begin(), loops.end(), [&](const PerimeterGeneratorLoop &loop) {
const TopOverlap overlap = classify_over_top(loop.polygon.points, one_wall_top_region, top_region_bbox);
if (overlap == TopOverlap::None)
return false;
// Only a wall straddling the boundary is worth measuring; a wall wholly over the top goes.
if (overlap == TopOverlap::Partial &&
total_length(intersection_pl(Polylines{ loop.polygon.split_at_first_point() }, one_wall_top_region)) < grazing_tolerance) {
append(one_wall_top_kept_bands, wall_band(loop.polygon));
return false;
}
append(dropped_wall_bands, wall_band(loop.polygon));
return true;
}), loops.end());
};
for (int d = 1; d <= loop_number; ++ d) {
reduce_over_top(contours[d]);
reduce_over_top(holes[d]);
}
if (! gaps.empty())
gaps = diff_ex(gaps, one_wall_top_region);
if (! dropped_wall_bands.empty())
one_wall_top_reclaimed = diff_ex(dropped_wall_bands, one_wall_top_region);
}
// nest loops: holes first
for (int d = 0; d <= loop_number; ++ d) {
PerimeterGeneratorLoops &holes_d = holes[d];
@@ -1619,7 +1843,10 @@ void PerimeterGenerator::process_classic()
and use zigzag). */
//FIXME Vojtech: This grows by a rounded extrusion width, not by line spacing,
// therefore it may cover the area, but no the volume.
last = diff_ex(last, gap_fill.polygons_covered_by_width(10.f));
Polygons gap_fill_covered = gap_fill.polygons_covered_by_width(10.f);
last = diff_ex(last, gap_fill_covered);
if (! one_wall_top_reclaimed.empty())
one_wall_top_reclaimed = diff_ex(one_wall_top_reclaimed, gap_fill_covered);
this->gap_fill->append(std::move(gap_fill.entities));
}
@@ -1664,9 +1891,15 @@ void PerimeterGenerator::process_classic()
// append infill areas to fill_surfaces
//if any top_fills, grow them by ext_perimeter_spacing/2 to have the real un-anchored fill
ExPolygons top_infill_exp = intersection_ex(fill_clip, offset_ex(top_fills, double(ext_perimeter_spacing / 2)));
// ORCA: only_one_wall_top - route the top fill around the walls kept despite grazing the top.
if (!one_wall_top_kept_bands.empty())
top_infill_exp = diff_ex(top_infill_exp, one_wall_top_kept_bands);
if (!top_fills.empty()) {
infill_exp = union_ex(infill_exp, offset_ex(top_infill_exp, double(top_infill_peri_overlap)));
}
// ORCA: only_one_wall_top - what the top fill does not cover of the dropped walls goes to infill.
if (!one_wall_top_reclaimed.empty())
infill_exp = union_ex(infill_exp, one_wall_top_reclaimed);
this->fill_surfaces->append(infill_exp, stInternal);
apply_extra_perimeters(infill_exp);
@@ -1685,6 +1918,8 @@ void PerimeterGenerator::process_classic()
double(-inset - infill_peri_overlap));
if (!top_fills.empty())
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_infill_exp);
if (!one_wall_top_reclaimed.empty())
polyWithoutOverlap = union_ex(polyWithoutOverlap, one_wall_top_reclaimed);
this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end());
}
@@ -1742,7 +1977,7 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
ExPolygons unsupported = diff_ex(last, *this->lower_slices, ApplySafetyOffset::Yes);
if (!unsupported.empty()) {
//remove small overhangs
ExPolygons unsupported_filtered = offset2_ex(unsupported, double(-perimeter_spacing), double(perimeter_spacing));
ExPolygons unsupported_filtered = opening_ex(unsupported, perimeter_spacing);
if (!unsupported_filtered.empty()) {
//to_draw.insert(to_draw.end(), last.begin(), last.end());
@@ -1855,35 +2090,40 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
//TODO: add other polys as holes inside this one (-margin)
} else { // if(this->config->counterbore_hole_bridging.value == chbBridges)
// Orca: Partial counterbore bridging is mask-based. Preserve the supported
// remainder (`last`) and use simplified BridgeDetector coverage to derive the
// remainder and use simplified BridgeDetector coverage to derive the
// bridgeable counterbore span. The span is grown from supported material,
// shrunk back, stripped from `last`, and expanded back. It is then prevented
// from intruding deeper into `last` than the explicit anchor overlap.
// Finally, add the allowed anchor band from `last` then remove the
// shrunk back, stripped from the remaining normal surface, and expanded back.
// It is then prevented from intruding deeper into it than the explicit anchor overlap.
// Finally, add the allowed anchor band from it then remove the
// narrow hole-side wall contact, which must remain unbridgeable.
last = diff_ex(last, unsupported_filtered, ApplySafetyOffset::Yes);
const ExPolygons remaining = diff_ex(last, unsupported_filtered, ApplySafetyOffset::Yes);
ExPolygons bridgeable_filtered;
for (ExPolygon& poly : bridgeable) {
poly.simplify(perimeter_spacing, &bridgeable_filtered);
}
bridgeable_filtered = opening_ex(bridgeable_filtered, ext_perimeter_width);
// Get rid of coarseness of the resulted bridgeable area by using the original supported area as reference.
// This is to avoid keeping tiny bridgeable areas that are far from the supported area, or protrude into it.
bridgeable_filtered = union_ex(offset_ex(last, perimeter_spacing), bridgeable_filtered);
// This is to avoid keeping tiny bridgeable areas that are far from the supported area, or protrude into it.
bridgeable_filtered = union_ex(offset_ex(remaining, perimeter_spacing), bridgeable_filtered);
bridgeable_filtered = offset_ex(bridgeable_filtered, -perimeter_spacing);
bridgeable_filtered = diff_ex(bridgeable_filtered, last, ApplySafetyOffset::Yes);
bridgeable_filtered = diff_ex(bridgeable_filtered, remaining, ApplySafetyOffset::Yes);
bridgeable_filtered = opening_ex(bridgeable_filtered, perimeter_spacing); // filter noise from the diff_ex
bridgeable_filtered = offset_ex(bridgeable_filtered, perimeter_spacing); // restore the size to the original bridgeable area
// Safety measure: Keep the bridge mask from intruding deeper into the
// supported anchor region (`last`) than the explicit anchor overlap.
bridgeable_filtered = diff_ex(bridgeable_filtered, offset_ex(last, -bridge_anchor_offset));
// supported anchor region than the explicit anchor overlap.
bridgeable_filtered = diff_ex(bridgeable_filtered, offset_ex(remaining, -bridge_anchor_offset));
ExPolygons bridge_anchor_areas = intersection_ex(last, offset_ex(unsupported_filtered, bridge_anchor_offset));
ExPolygons bridge_anchor_areas = intersection_ex(remaining, offset_ex(unsupported_filtered, bridge_anchor_offset));
unsupported_filtered = union_ex(bridgeable_filtered, bridge_anchor_areas); // add bridge anchor
unsupported_filtered = opening_ex(unsupported_filtered, bridge_anchor_offset); // remove anchor area from hole-side walls, it must remain unbridgeable
// update 'last' only if we have a valid bridgeable area, otherwise we will lose the original unsupported area
if (!unsupported_filtered.empty())
last = remaining;
// TODO: Fix the case with thin outer walls around the bridge (1~2 walls) where classic wall
// might generate two walls in a tiny space or non at all if "Detect thin walls" is not activated
}
@@ -2123,6 +2363,11 @@ void PerimeterGenerator::process_arachne()
process_no_bridge(all_surfaces, perimeter_spacing, ext_perimeter_width);
// BBS: don't simplify too much which influence arc fitting when export gcode if arc_fitting is enabled
double surface_simplify_resolution = (print_config->enable_arc_fitting && !this->has_fuzzy_skin) ? 0.2 * m_scaled_resolution : m_scaled_resolution;
// ORCA: neither one-wall option has a surface to act on without the shell behind it, see
// has_top_shell_layers() / has_bottom_shell_layers(). Gated here so every use below - including the
// topmost and first layers - sees the same answer.
const bool only_one_wall_top = this->config->only_one_wall_top && has_top_shell_layers(*this->config);
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
// we need to process each island separately because we might have different
// extra perimeters for each one
for (const Surface& surface : all_surfaces) {
@@ -2135,12 +2380,12 @@ void PerimeterGenerator::process_arachne()
// Set the bottommost layer to be one wall
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
if (is_bottom_layer && this->config->only_one_wall_first_layer)
if (is_bottom_layer && only_one_wall_first_layer)
loop_number = 0;
// Orca: set the topmost layer to be one wall according to the config
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
if (is_topmost_layer && loop_number > 0 && config->only_one_wall_top)
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
loop_number = 0;
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
@@ -2160,10 +2405,10 @@ void PerimeterGenerator::process_arachne()
//PS: One wall top surface for Arachne
ExPolygons top_expolygons;
// Calculate how many inner loops remain when TopSurfaces is selected.
const int inner_loop_number = (config->only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
// Set one perimeter when TopSurfaces is selected.
if (config->only_one_wall_top && loop_number > 0)
if (only_one_wall_top && loop_number > 0)
loop_number = 0;
Arachne::WallToolPathsParams input_params_tmp = input_params;
@@ -2214,25 +2459,33 @@ void PerimeterGenerator::process_arachne()
// due to thin lines being generated
top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
// Get the not-top ExPolygons (including bridges) from current slices and expanded real top ExPolygons (without bridges).
const ExPolygons not_top_expolygons = diff_ex(infill_contour, top_expolygons);
// Get final top ExPolygons.
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
top_expolygons = intersection_ex(top_expolygons, infill_contour);
const Polygons not_top_polygons = to_polygons(offset_ex(not_top_expolygons,wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(not_top_polygons, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
// top-surface islands with inner walls that don't exist when the feature is disabled.
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
: diff_ex(infill_contour, top_expolygons),
wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
// Recalculate indexes of inner perimeters before merging them.
if (!perimeters.empty()) {
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) {
if (inner_perimeter.empty())
continue;
if (clip_walls_over_top) {
Polygons kept_over_top;
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
// Route the top fill around the walls kept despite grazing the top.
if (! kept_over_top.empty())
top_expolygons = diff_ex(top_expolygons, kept_over_top);
}
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
if (!perimeters.empty())
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
for (Arachne::ExtrusionLine &el : inner_perimeter)
++el.inset_idx;
}
}
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
+25
View File
@@ -712,6 +712,26 @@ namespace client
static void regex_matches (expr &lhs, IteratorRange &rhs) { return regex_op(lhs, rhs, '=', lhs); }
static void regex_doesnt_match(expr &lhs, IteratorRange &rhs) { return regex_op(lhs, rhs, '!', lhs); }
// Replace every match of the regular expression 'pattern' in the string 'subject' with 'replacement'.
// The replacement may reference capture groups ($1, $2, ...). Store the result into subject.
static void regex_replace(expr &subject, IteratorRange &pattern, expr &replacement)
{
if (subject.type() == TYPE_EMPTY)
// Inside an if / else block to be skipped
return;
if (subject.type() != TYPE_STRING)
subject.throw_exception("regex_replace() first parameter must be a string.");
try {
std::string re(++ pattern.begin(), -- pattern.end());
std::string result = SLIC3R_REGEX_NAMESPACE::regex_replace(subject.s(), SLIC3R_REGEX_NAMESPACE::regex(re), replacement.to_string());
subject.set_s(std::move(result));
} catch (SLIC3R_REGEX_NAMESPACE::regex_error &ex) {
// Syntax error in the regular expression
boost::throw_exception(qi::expectation_failure<Iterator>(
pattern.begin(), pattern.end(), spirit::info(std::string("*Regular expression compilation failed: ") + ex.what())));
}
}
static void one_of_test_init(expr &out) {
out.set_b(false);
}
@@ -1771,6 +1791,8 @@ namespace client
// from UTF8 to UTF16 don't bail out.
msg += boost::nowide::narrow(boost::nowide::widen(error_line));
msg += '\n';
// The error dialog (MsgDialog.cpp) renders this excerpt monospaced. It recognizes a source
// line directly above a caret line of spaces and a single '^'.
for (size_t i = 0; i < error_pos; ++ i)
msg += ' ';
msg += "^\n";
@@ -2323,6 +2345,8 @@ namespace client
[ px::bind(&expr::digits<false>, _val, _2, _3) ]
| (kw["zdigits"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > optional_parameter(_r1))
[ px::bind(&expr::digits<true>, _val, _2, _3) ]
| (kw["regex_replace"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > regular_expression > ',' > conditional_expression(_r1) > ')')
[ px::bind(&expr::regex_replace, _val, _2, _3) ]
| (kw["int"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::to_int, _1, _val) ]
| (kw["round"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::round, _1, _val) ]
| (kw["ceil"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::ceil, _1, _val) ]
@@ -2404,6 +2428,7 @@ namespace client
("min")
("max")
("random")
("regex_replace")
("filament_change")
("repeat")
("round")
+26
View File
@@ -105,6 +105,32 @@ PlatformFlavor platform_flavor()
return s_platform_flavor;
}
std::string platform_os_type()
{
#if defined(_WIN32)
return "win";
#elif defined(__APPLE__)
return "macos";
#elif defined(__linux__) || defined(__LINUX__)
return "linux";
#else
return "unknown";
#endif
}
std::string platform_architecture()
{
#if defined(__aarch64__) || defined(__arm64__) || defined(_M_ARM64)
return "arm64";
#elif defined(__x86_64__) || defined(__x86_64) || defined(__amd64__) || defined(__amd64) || defined(_M_X64) || defined(_M_AMD64)
return "x86_64";
#elif defined(__i386__) || defined(__i386) || defined(i386) || defined(_M_IX86)
return "i386";
#else
return "unknown";
#endif
}
std::string platform_to_string(Platform platform)
+2
View File
@@ -36,6 +36,8 @@ void detect_platform();
Platform platform();
PlatformFlavor platform_flavor();
std::string platform_os_type();
std::string platform_architecture();
std::string platform_to_string(Platform platform);
std::string platform_flavor_to_string(PlatformFlavor pf);
+2 -2
View File
@@ -129,13 +129,13 @@ public:
std::vector<PathFittingData> fitting_result;
//BBS: simplify points by arc fitting
void simplify_by_fitting_arc(double tolerance);
//BBS:
void reset_to_linear_move();
//BBS:
Polylines equally_spaced_lines(double distance) const;
private:
void append_fitting_result_after_append_points();
void append_fitting_result_after_append_polyline(const Polyline& src);
void reset_to_linear_move();
bool split_fitting_result_before_index(const size_t index, Point &new_endpoint, std::vector<PathFittingData>& data) const;
bool split_fitting_result_after_index(const size_t index, Point &new_startpoint, std::vector<PathFittingData>& data) const;
};
+114 -25
View File
@@ -252,7 +252,7 @@ void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil
auto replace_nil_and_resize = [&](const std::string & key, int length){
ConfigOption* raw_ptr = config.option(key);
ConfigOptionVectorBase* opt_vec = static_cast<ConfigOptionVectorBase *>(raw_ptr);
if(set_nil_to_default && raw_ptr->is_nil() && defaults.has(key) && std::find(filament_extruder_override_keys.begin(), filament_extruder_override_keys.end(), key) == filament_extruder_override_keys.end()){
if(set_nil_to_default && raw_ptr->is_nil() && defaults.has(key) && !is_filament_extruder_override_key(key)){
opt_vec->clear();
opt_vec->resize(length, defaults.option(key));
}
@@ -470,6 +470,8 @@ void Preset::normalize(DynamicPrintConfig &config)
continue;
if (filament_options_with_variant.find(key) != filament_options_with_variant.end())
continue;
if (filament_dev_options.find(key) != filament_dev_options.end())
continue;
auto *opt = config.option(key, false);
/*assert(opt != nullptr);
assert(opt->is_vector());*/
@@ -944,12 +946,11 @@ std::string Preset::get_printer_type(PresetBundle *preset_bundle)
{
if (preset_bundle) {
auto config = &preset_bundle->printers.get_edited_preset().config;
std::string vendor_name;
for (auto vendor_profile : preset_bundle->vendors) {
for (auto vendor_model : vendor_profile.second.models)
if (vendor_model.name == config->opt_string("printer_model"))
const auto& printer_model = config->opt_string("printer_model");
for (const auto& vendor_profile : preset_bundle->vendors) {
for (const auto& vendor_model : vendor_profile.second.models)
if (vendor_model.name == printer_model)
{
vendor_name = vendor_profile.first;
return vendor_model.model_id;
}
}
@@ -961,11 +962,10 @@ std::string Preset::get_current_printer_type(PresetBundle *preset_bundle)
{
if (preset_bundle) {
auto config = &(this->config);
std::string vendor_name;
for (auto vendor_profile : preset_bundle->vendors) {
for (auto vendor_model : vendor_profile.second.models)
if (vendor_model.name == config->opt_string("printer_model")) {
vendor_name = vendor_profile.first;
const auto& printer_model = config->opt_string("printer_model");
for (const auto& vendor_profile : preset_bundle->vendors) {
for (const auto& vendor_model : vendor_profile.second.models)
if (vendor_model.name == printer_model) {
return vendor_model.model_id;
}
}
@@ -1091,6 +1091,7 @@ static std::vector<std::string> s_Preset_print_options{
"fill_multiline",
"gyroid_optimized",
"sparse_infill_pattern",
"sparse_infill_smooth_factor",
"lateral_lattice_angle_1",
"lateral_lattice_angle_2",
"infill_overhang_angle",
@@ -1098,7 +1099,12 @@ static std::vector<std::string> s_Preset_print_options{
"lightning_prune_angle",
"lightning_straightening_angle",
"top_surface_pattern",
"top_surface_expansion",
"top_surface_expansion_margin",
"top_surface_expansion_direction",
"bottom_surface_pattern",
"top_surface_fill_order",
"bottom_surface_fill_order",
"infill_direction",
"solid_infill_direction",
"top_layer_direction",
@@ -1114,6 +1120,8 @@ static std::vector<std::string> s_Preset_print_options{
"skin_infill_density",
"align_infill_direction_to_model",
"extra_solid_infills",
"center_of_surface_pattern",
"separated_infills",
"minimum_sparse_infill_area",
"reduce_infill_retraction",
"internal_solid_infill_pattern",
@@ -1137,7 +1145,7 @@ static std::vector<std::string> s_Preset_print_options{
"top_surface_speed", "support_speed", "support_object_xy_distance", "support_object_first_layer_gap", "support_interface_speed",
"bridge_speed", "internal_bridge_speed", "gap_infill_speed", "travel_speed", "travel_speed_z", "initial_layer_speed",
"outer_wall_acceleration", "initial_layer_acceleration", "top_surface_acceleration", "default_acceleration", "skirt_type", "skirt_loops", "skirt_speed","min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_height","single_loop_draft_shield", "draft_shield",
"brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers",
"brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "brim_ears_outer_only", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers",
"raft_layers", "raft_first_layer_density", "raft_first_layer_expansion", "raft_contact_distance", "raft_expansion",
"support_base_pattern", "support_base_pattern_spacing", "support_expansion", "support_style",
// BBS
@@ -1248,6 +1256,9 @@ static std::vector<std::string> s_Preset_print_options{
"min_feature_size",
"min_bead_width",
"post_process",
"slicing_pipeline_plugin",
"plugins",
"print_plugin_config_overrides",
"process_change_extrusion_role_gcode",
"min_length_factor",
"wall_maximum_resolution",
@@ -1326,6 +1337,7 @@ static std::vector<std::string> s_Preset_print_options{
"wipe_tower_bridging",
"wipe_tower_extra_flow",
"single_extruder_multi_material_priming",
"toolchange_ordering",
"wipe_tower_rotation_angle",
"tree_support_branch_distance_organic",
"tree_support_branch_diameter_organic",
@@ -1333,6 +1345,7 @@ static std::vector<std::string> s_Preset_print_options{
"hole_to_polyhole",
"hole_to_polyhole_threshold",
"hole_to_polyhole_twisted",
"hole_to_polyhole_max_edges",
"mmu_segmented_region_max_width",
"mmu_segmented_region_interlocking_depth",
"small_area_infill_flow_compensation",
@@ -1365,7 +1378,7 @@ static std::vector<std::string> s_Preset_print_options{
};
static std::vector<std::string> s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type",
"filament_soluble", "filament_is_support", "filament_printable",
"filament_soluble", "filament_is_support", "filament_printable", "filament_extruder_compatibility",
"filament_max_volumetric_speed", "filament_adaptive_volumetric_speed",
"filament_flow_ratio", "filament_density", "filament_adhesiveness_category", "filament_cost", "filament_minimal_purge_on_wipe_tower",
"filament_tower_interface_pre_extrusion_dist", "filament_tower_interface_pre_extrusion_length", "filament_tower_ironing_area", "filament_tower_interface_purge_volume",
@@ -1381,8 +1394,22 @@ static std::vector<std::string> s_Preset_filament_options {/*"filament_colour",
//exhaust fan control
"activate_air_filtration","activate_air_filtration_during_print","activate_air_filtration_on_completion","during_print_exhaust_fan_speed","complete_print_exhaust_fan_speed",
// Retract overrides
"filament_retraction_length", "filament_z_hop", "filament_z_hop_types", "filament_retract_lift_above", "filament_retract_lift_below", "filament_retract_lift_enforce", "filament_retraction_speed", "filament_deretraction_speed", "filament_retract_restart_extra", "filament_retraction_minimum_travel",
"filament_retract_when_changing_layer", "filament_wipe", "filament_retract_before_wipe",
"filament_deretraction_speed",
"filament_retract_after_wipe", // Orca
"filament_retract_before_wipe",
"filament_retract_lift_above",
"filament_retract_lift_below",
"filament_retract_lift_enforce",
"filament_retract_restart_extra",
"filament_retract_when_changing_layer",
"filament_retraction_length",
"filament_retraction_minimum_travel",
"filament_retraction_speed",
"filament_retract_length_toolchange",
"filament_retract_restart_extra_toolchange",
"filament_wipe",
"filament_z_hop",
"filament_z_hop_types",
// Profile compatibility
"filament_vendor", "compatible_prints", "compatible_prints_condition", "compatible_printers", "compatible_printers_condition", "inherits",
//BBS
@@ -1400,8 +1427,19 @@ static std::vector<std::string> s_Preset_filament_options {/*"filament_colour",
"filament_multitool_ramming", "filament_multitool_ramming_volume", "filament_multitool_ramming_flow", "activate_chamber_temp_control", "chamber_minimal_temperature",
"filament_long_retractions_when_cut","filament_retraction_distances_when_cut", "idle_temperature",
//BBS filament change length while the extruder color
"filament_change_length","filament_flush_volumetric_speed","filament_flush_temp", "filament_cooling_before_tower",
"long_retractions_when_ec", "retraction_distances_when_ec"
"filament_change_length","filament_flush_volumetric_speed","filament_flush_temp","filament_flush_temp_fast", "filament_cooling_before_tower",
// Multi-nozzle pre-cooling / ramming / nozzle-change (nc) filament overrides
"filament_ramming_volumetric_speed", "filament_ramming_volumetric_speed_nc",
"filament_ramming_travel_time", "filament_ramming_travel_time_nc",
"filament_pre_cooling_temperature", "filament_pre_cooling_temperature_nc",
"filament_preheat_temperature_delta", "filament_retract_length_nc",
"filament_change_length_nc", "filament_prime_volume", "filament_prime_volume_nc",
"long_retractions_when_ec", "retraction_distances_when_ec",
"filament_plugin_config_overrides",
//ams chamber
"filament_dev_ams_drying_ams_limitations", "filament_dev_ams_drying_temperature", "filament_dev_ams_drying_time", "filament_dev_ams_drying_heat_distortion_temperature",
"filament_dev_chamber_drying_bed_temperature", "filament_dev_chamber_drying_time",
"filament_dev_drying_softening_temperature", "filament_dev_drying_cooling_temperature"
};
static std::vector<std::string> s_Preset_machine_limits_options {
@@ -1411,6 +1449,8 @@ static std::vector<std::string> s_Preset_machine_limits_options {
"machine_min_extruding_rate", "machine_min_travel_rate",
"machine_max_jerk_x", "machine_max_jerk_y", "machine_max_jerk_z", "machine_max_jerk_e",
"machine_max_junction_deviation",
// Bedslinger mass/force limits
"machine_max_force_Y", "machine_bed_mass_Y", "machine_max_printed_mass",
//resonance avoidance ported from qidi slicer
"resonance_avoidance", "min_resonance_avoidance_speed", "max_resonance_avoidance_speed",
// Orca: input shaping
@@ -1420,7 +1460,7 @@ static std::vector<std::string> s_Preset_machine_limits_options {
static std::vector<std::string> s_Preset_printer_options {
"printer_technology",
"printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor",
"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",
"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",
@@ -1428,7 +1468,7 @@ static std::vector<std::string> s_Preset_printer_options {
"default_print_profile", "inherits",
"silent_mode",
"scan_first_layer", "enable_power_loss_recovery", "wrapping_detection_layers", "wrapping_exclude_area", "machine_load_filament_time", "machine_unload_filament_time", "machine_tool_change_time", "time_cost", "machine_pause_gcode", "template_custom_gcode",
"nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "travel_slope", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","printer_structure",
"nozzle_type", "nozzle_hrc","auxiliary_fan", "fan_direction", "nozzle_volume","upward_compatible_machine", "z_hop_types", "travel_slope", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","support_cooling_filter","cooling_filter_enabled","printer_structure","farthest_point_timelapse",
"best_object_pos", "head_wrap_detect_zone",
"host_type", "print_host", "printhost_apikey", "flashforge_serial_number", "bbl_use_printhost", "printer_agent",
"print_host_webui",
@@ -1437,10 +1477,17 @@ static std::vector<std::string> s_Preset_printer_options {
"use_relative_e_distances", "extruder_type", "use_firmware_retraction", "printer_notes",
"grab_length", "support_object_skip_flush", "physical_extruder_map",
"cooling_tube_retraction",
"cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower",
"cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower", "wait_for_temp_on_wipe_tower",
"z_offset",
"disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "use_3mf", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut",
"bed_temperature_formula", "nozzle_flush_dataset"
"bed_temperature_formula", "nozzle_flush_dataset",
// Multi-nozzle count + pre-heat model printer options
"extruder_max_nozzle_count", "group_algo_with_time", "enable_pre_heating", "hotend_heating_rate", "hotend_cooling_rate",
"machine_hotend_change_time", "machine_prepare_compensation_time",
// Fast-purge printer flag + device/firmware-facing per-variant extruder-change
// deretraction speed (unconsumed by the slicer; carried by H2D/A2L/X2D/P2S machine profiles).
"support_fast_purge_mode", "deretract_speed_extruder_change",
"printer_plugin_config_overrides"
};
static std::vector<std::string> s_Preset_sla_print_options {
@@ -1552,6 +1599,15 @@ const std::vector<std::string>& Preset::printer_options()
return s_opts;
}
const char* Preset::plugin_overrides_key(Type type)
{
switch (type) {
case TYPE_PRINTER: return "printer_plugin_config_overrides";
case TYPE_FILAMENT: return "filament_plugin_config_overrides";
default: return "print_plugin_config_overrides";
}
}
PresetCollection::PresetCollection(Preset::Type type, const std::vector<std::string> &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &default_name) :
m_type(type),
m_edited_preset(type, "", false),
@@ -3468,7 +3524,18 @@ void add_correct_opts_to_diff(const std::string &opt_key, t_config_option_keys&
for (int i = 0; i < int(opt_cur->values.size()); i++)
{
int init_id = i <= opt_init_max_id ? i : 0;
const bool is_new_index = i > opt_init_max_id;
int init_id = is_new_index ? 0 : i;
if (is_new_index) {
// Orca: intentional divergence from upstream. Any new vector index (at or
// beyond the reference vector's length) is flagged dirty unconditionally --
// independent of its value and nil-state -- so preset dirty-detection notices
// per-extruder/filament entries added by growth (e.g. extruder count). This
// applies to every vector option type routed through deep_diff().
// Covered by tests/libslic3r/test_preset_diff.cpp.
vec.emplace_back(opt_key + "#" + std::to_string(i));
continue;
}
if (opt_cur->values[i] != opt_init->values[init_id]) {
if (opt_cur->nullable()) {
if (opt_cur->is_nil(i)) {
@@ -3503,7 +3570,7 @@ inline t_config_option_keys deep_diff(const ConfigBase &config_this, const Confi
if (this_opt != nullptr && other_opt != nullptr && *this_opt != *other_opt)
{
//BBS: add bed_exclude_area
if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "thumbnails" || opt_key == "wrapping_exclude_area") {
if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "thumbnails" || opt_key == "wrapping_exclude_area" || opt_key == "slicing_pipeline_plugin") {
// Scalar variable, or a vector variable, which is independent from number of extruders,
// thus the vector is presented to the user as a single input.
diff.emplace_back(opt_key);
@@ -3773,12 +3840,14 @@ void PresetCollection::update_library_profile_excluded_from()
}
// Check all presets that has the same alias as the filament presets with empty compatible_printers in Orca Filament Library.
// A printer specific profile supersedes the generic one, no matter whether it lives in a vendor bundle or in the
// library itself.
for (const Preset& preset : m_presets) {
if (preset.vendor == nullptr || preset.vendor->name == PresetBundle::ORCA_FILAMENT_LIBRARY)
if (preset.vendor == nullptr)
continue;
const auto* compatible_printers = dynamic_cast<const ConfigOptionStrings*>(preset.config.option("compatible_printers"));
// All profiles in concrete vendor profile shouldn't have empty compatible_printers, but here we check it for safety.
// Profiles with empty compatible_printers are the generic ones, they never supersede anything.
if (compatible_printers == nullptr || compatible_printers->values.empty())
continue;
auto itr = excluded_froms.find(preset.alias);
@@ -3834,6 +3903,26 @@ void PresetCollection::set_custom_preset_alias(Preset &preset)
set_printer_hold_alias(preset.alias, preset);
}
std::string PresetCollection::get_preset_alias(Preset &preset, bool force)
{
if (!preset.alias.empty())
return preset.alias;
else
set_custom_preset_alias(preset);
if (!preset.alias.empty() || !force)
return preset.alias;
std::string alias_name;
std::string preset_name = preset.name;
size_t end_pos = preset_name.find_first_of("@");
if (end_pos != std::string::npos) {
alias_name = preset_name.substr(0, end_pos);
boost::trim_right(alias_name);
}
return alias_name;
}
void PresetCollection::set_printer_hold_alias(const std::string &alias, Preset &preset, bool remove)
{
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset.config.option("compatible_printers"));
+10
View File
@@ -72,6 +72,7 @@
#define BBL_JSON_KEY_BOTTOM_TEXTURE_END_NAME "bottom_texture_end_name"
#define BBL_JSON_KEY_USE_DOUBLE_EXTRUDER_DEFAULT_TEXTURE "use_double_extruder_default_texture"
#define BBL_JSON_KEY_BOTTOM_TEXTURE_RECT "bottom_texture_rect"
#define BBL_JSON_KEY_BOTTOM_TEXTURE_RECT_LONGER "bottom_texture_rect_longer"
#define BBL_JSON_KEY_MIDDLE_TEXTURE_RECT "middle_texture_rect"
#define BBL_JSON_KEY_HOTEND_MODEL "hotend_model"
@@ -167,6 +168,7 @@ public:
std::string bottom_texture_end_name;
std::string use_double_extruder_default_texture;
std::string bottom_texture_rect;
std::string bottom_texture_rect_longer;
std::string middle_texture_rect;
std::string hotend_model;
PrinterVariant* variant(const std::string &name) {
@@ -422,6 +424,11 @@ public:
// Printer machine limits, those are contained in printer_options().
static const std::vector<std::string>& machine_limits_options();
// Option key holding this preset type's plugin capability overrides. Each type has its own key so
// the values survive the merge into a single full config; print is the fallback for the types with
// no plugin-backed options.
static const char* plugin_overrides_key(Type type);
static const std::vector<std::string>& sla_printer_options();
static const std::vector<std::string>& sla_material_options();
static const std::vector<std::string>& sla_print_options();
@@ -820,6 +827,9 @@ public:
std::string path_from_name(const std::string &new_name, bool detach = false) const;
std::string path_for_preset(const Preset & preset) const;
// Get the alias of a preset, setting it if it's empty
std::string get_preset_alias(Preset &preset, bool force = false);
size_t num_default_presets() { return m_num_default_presets; }
protected:
+190 -26
View File
@@ -52,9 +52,23 @@ static std::vector<std::string> s_project_options {
"wipe_tower_rotation_angle",
"curr_bed_type",
"flush_multiplier",
// Fast-purge mode: project-level purge control, inert at Default.
"flush_multiplier_fast",
"prime_volume_mode",
"nozzle_volume_type",
"filament_map_mode",
"filament_map"
"filament_map",
// Per-filament nozzle-volume choice; project-level like filament_map so the per-filament
// slot resolution survives preset switches.
"filament_volume_map",
// Per-filament physical-nozzle choice the grouping engine writes back; project-level so a
// saved project round-trips the assignment alongside filament_map/filament_volume_map.
"filament_nozzle_map",
// Filament Track Switch device state: whether the switch is installed and ready, and
// whether dynamic per-nozzle filament mapping is active. Persisted with the project and
// restored from a saved 3mf; reset to false on load and set true only by live device sync.
"has_filament_switcher",
"enable_filament_dynamic_map"
};
//Orca: add custom as default
@@ -71,7 +85,8 @@ DynamicPrintConfig PresetBundle::construct_full_config(
const DynamicPrintConfig& project_config,
std::vector<Preset>& in_filament_presets,
bool apply_extruder,
std::optional<std::vector<int>> filament_maps_new)
std::optional<std::vector<int>> filament_maps_new,
std::optional<std::vector<int>> filament_volume_maps_new)
{
DynamicPrintConfig &printer_config = in_printer_preset.config;
DynamicPrintConfig &print_config = in_print_preset.config;
@@ -86,12 +101,23 @@ DynamicPrintConfig PresetBundle::construct_full_config(
size_t num_filaments = in_filament_presets.size();
std::vector<int> filament_maps = out.option<ConfigOptionInts>("filament_map")->values;
std::vector<int> filament_volume_maps(num_filaments, (int)nvtStandard);
ConfigOptionInts* filament_volume_map_opt = out.option<ConfigOptionInts>("filament_volume_map");
if (filament_maps_new.has_value())
filament_maps = *filament_maps_new;
if (filament_volume_maps_new.has_value())
filament_volume_maps = *filament_volume_maps_new;
else if (filament_volume_map_opt && filament_volume_map_opt->values.size() == num_filaments)
filament_volume_maps = filament_volume_map_opt->values;
// in some middle state, they may be different
if (filament_maps.size() != num_filaments) {
filament_maps.resize(num_filaments, 1);
}
if (filament_volume_maps.size() != num_filaments) {
filament_volume_maps.resize(num_filaments, nvtStandard);
}
auto *extruder_diameter = dynamic_cast<const ConfigOptionFloats *>(out.option("nozzle_diameter"));
// Collect the "compatible_printers_condition" and "inherits" values over all presets (print, filaments, printers) into a single vector.
@@ -112,17 +138,34 @@ DynamicPrintConfig PresetBundle::construct_full_config(
inherits.emplace_back(print_inherits);
// BBS: update printer config related with variants
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
int extruder_count = 1, extruder_volume_type_count = 1;
bool different_extruder = false;
if (apply_extruder) {
out.update_values_to_printer_extruders(out, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant");
out.update_values_to_printer_extruders(out, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2);
// update print config related with variants
out.update_values_to_printer_extruders(out, print_options_with_variant, "print_extruder_id", "print_extruder_variant");
different_extruder = out.support_different_extruders(extruder_count);
extruder_volume_type_count = out.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
if ((extruder_count > 1) || different_extruder) {
// Orca: keep processing variant_1 before variant_2 here; variant_2 slots are resolved
// against the printer id/variant lists as rewritten by the variant_1 pass, and the
// composed values depend on that order. Note the order is load-bearing, not correct
// in general: the variant_2 pass reads the original full-width arrays through indices
// resolved on the shrunk lists, which mis-reads presets whose variant_2 columns differ
// per variant (e.g. X2D machine_max_speed_e/machine_max_acceleration_e). The slicing
// path composes variant_2 first and is unaffected; changing the order here would alter
// long-standing composed values, so any fix must re-baseline them.
out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant");
out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2);
// update print config related with variants
out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant");
}
}
if (num_filaments <= 1) {
// BBS: update filament config related with variants
DynamicPrintConfig filament_config = in_filament_presets[0].config;
if (apply_extruder) filament_config.update_values_to_printer_extruders(out, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0]);
if (apply_extruder && ((extruder_count > 1) || different_extruder))
filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]);
out.apply(filament_config);
compatible_printers_condition.emplace_back(in_filament_presets[0].compatible_printers_condition());
compatible_prints_condition.emplace_back(in_filament_presets[0].compatible_prints_condition());
@@ -145,8 +188,8 @@ DynamicPrintConfig PresetBundle::construct_full_config(
filament_temp_configs.resize(num_filaments);
for (size_t i = 0; i < num_filaments; ++i) {
filament_temp_configs[i] = *(filament_configs[i]);
if (apply_extruder)
filament_temp_configs[i].update_values_to_printer_extruders(out, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i]);
if (apply_extruder && ((extruder_count > 1) || different_extruder))
filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]);
}
// loop through options and apply them to the resulting config.
@@ -221,6 +264,7 @@ DynamicPrintConfig PresetBundle::construct_full_config(
out.option<ConfigOptionString>("printer_settings_id", true)->value = in_printer_preset.name;
out.option<ConfigOptionStrings>("filament_ids", true)->values = filament_ids;
out.option<ConfigOptionInts>("filament_map", true)->values = filament_maps;
out.option<ConfigOptionInts>("filament_volume_map", true)->values = filament_volume_maps;
auto add_if_some_non_empty = [&out](std::vector<std::string> &&values, const std::string &key) {
bool nonempty = false;
@@ -348,7 +392,7 @@ PresetBundle::PresetBundle()
auto& default_config = this->filaments.default_preset().config;
for(const std::string& opt_key : default_config.keys()){
ConfigOption* opt = default_config.optptr(opt_key, false);
bool is_override_key = std::find(filament_extruder_override_keys.begin(),filament_extruder_override_keys.end(), opt_key) != filament_extruder_override_keys.end();
bool is_override_key = is_filament_extruder_override_key(opt_key);
if(!is_override_key || !opt->nullable())
continue;
opt->deserialize("nil",ForwardCompatibilitySubstitutionRule::Disable);
@@ -721,6 +765,8 @@ std::optional<FilamentBaseInfo> PresetBundle::get_filament_by_filament_id(const
auto iter = std::find(compatible_printers.begin(), compatible_printers.end(), printer_name);
if (iter != compatible_printers.end() && config.has("filament_printable")) {
info.filament_printable = config.option<ConfigOptionInts>("filament_printable")->values[0];
if (config.has("filament_extruder_compatibility"))
info.set_filament_extruder_compatibility(config.option<ConfigOptionInts>("filament_extruder_compatibility")->values[0]);
return info;
}
}
@@ -2680,6 +2726,12 @@ void PresetBundle::update_selections(AppConfig &config)
std::vector<int> filament_maps(filament_colors.size(), 1);
project_config.option<ConfigOptionInts>("filament_map")->values = filament_maps;
std::vector<int> filament_nozzle_maps(filament_colors.size(), 0);
project_config.option<ConfigOptionInts>("filament_nozzle_map")->values = filament_nozzle_maps;
std::vector<int> filament_volume_maps(filament_colors.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
project_config.option<ConfigOptionInts>("filament_volume_map")->values = filament_volume_maps;
std::vector<std::string> extruder_ams_count_str;
if (config.has_printer_setting(initial_printer_profile_name, "extruder_ams_count")) {
boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), boost::algorithm::is_any_of(","));
@@ -2824,6 +2876,12 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p
std::vector<int> filament_maps(filament_colors.size(), 1);
project_config.option<ConfigOptionInts>("filament_map")->values = filament_maps;
std::vector<int> filament_nozzle_maps(filament_colors.size(), 0);
project_config.option<ConfigOptionInts>("filament_nozzle_map")->values = filament_nozzle_maps;
std::vector<int> filament_volume_maps(filament_colors.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
project_config.option<ConfigOptionInts>("filament_volume_map")->values = filament_volume_maps;
std::vector<std::string> extruder_ams_count_str;
if (config.has_printer_setting(initial_printer_profile_name, "extruder_ams_count")) {
boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), boost::algorithm::is_any_of(","));
@@ -3000,7 +3058,8 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector<std::string> ne
ConfigOptionStrings *filament_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
ConfigOptionStrings* filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts* filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts* filament_nozzle_map = project_config.option<ConfigOptionInts>("filament_nozzle_map");
ConfigOptionInts* filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
filament_color->resize(n);
// Sync filament multi colour
@@ -3010,6 +3069,8 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector<std::string> ne
}
filament_color_type->resize(n);
filament_map->values.resize(n, 1);
filament_nozzle_map->values.resize(n, 0);
filament_volume_map->values.resize(n, static_cast<int>(NozzleVolumeType::nvtStandard));
ams_multi_color_filment.resize(n);
// BBS set new filament color to new_color
@@ -3037,7 +3098,8 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color)
ConfigOptionStrings *filament_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
ConfigOptionStrings* filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts* filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts* filament_nozzle_map = project_config.option<ConfigOptionInts>("filament_nozzle_map");
ConfigOptionInts* filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
filament_color->resize(n);
// Sync filament multi colour
@@ -3047,6 +3109,8 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color)
}
filament_color_type->resize(n);
filament_map->values.resize(n, 1);
filament_nozzle_map->values.resize(n, 0);
filament_volume_map->values.resize(n, static_cast<int>(NozzleVolumeType::nvtStandard));
ams_multi_color_filment.resize(n);
//BBS set new filament color to new_color
@@ -3087,15 +3151,25 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id)
ConfigOptionStrings *filament_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts* filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts* filament_nozzle_map = project_config.option<ConfigOptionInts>("filament_nozzle_map");
ConfigOptionInts* filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
if (filament_color->values.size() > to_del_flament_id) {
filament_color->values.erase(filament_color->values.begin() + to_del_flament_id);
if (filament_map->values.size() > to_del_flament_id) {
filament_map->values.erase(filament_map->values.begin() + to_del_flament_id);
}
if (filament_nozzle_map->values.size() > to_del_flament_id) {
filament_nozzle_map->values.erase(filament_nozzle_map->values.begin() + to_del_flament_id);
}
if (filament_volume_map->values.size() > to_del_flament_id) {
filament_volume_map->values.erase(filament_volume_map->values.begin() + to_del_flament_id);
}
}
else {
filament_color->values.resize(to_del_flament_id);
filament_map->values.resize(to_del_flament_id, 1);
filament_nozzle_map->values.resize(to_del_flament_id, 0);
filament_volume_map->values.resize(to_del_flament_id, static_cast<int>(NozzleVolumeType::nvtStandard));
}
// lambda function to erase or resize the container
@@ -3357,6 +3431,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ConfigOptionStrings *filament_color = project_config.option<ConfigOptionStrings>("filament_colour");
ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts * filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts * filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
if (color_only) {
auto get_map_index = [&ams_infos](const std::vector<AMSMapInfo> &infos, const AMSMapInfo &temp) {
for (int i = 0; i < infos.size(); i++) {
@@ -3538,6 +3613,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ams_multi_color_filment = exist_multi_color_filment;
this->filament_presets = exist_filament_presets;
filament_map->values.resize(exist_filament_presets.size(), 1);
filament_volume_map->values.resize(exist_filament_presets.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
}
else {//overwrite;
bool has_placeholders = std::any_of(ams_infos.begin(), ams_infos.end(),
@@ -3592,12 +3668,14 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
this->filament_presets = result_presets;
ams_multi_color_filment = result_multi_colors;
filament_map->values.resize(total, 1);
filament_volume_map->values.resize(total, static_cast<int>(NozzleVolumeType::nvtStandard));
} else {
// BBL: existing wholesale replace
filament_color->values = ams_filament_colors;
filament_color_type->values = ams_filament_color_types;
this->filament_presets = ams_filament_presets;
filament_map->values.resize(ams_filament_colors.size(), 1);
filament_volume_map->values.resize(ams_filament_colors.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
}
auto& print_config = this->prints.get_edited_preset().config;
@@ -3904,10 +3982,26 @@ bool PresetBundle::support_different_extruders() const
return supported;
}
DynamicPrintConfig PresetBundle::full_config(bool apply_extruder, std::optional<std::vector<int>>filament_maps) const
std::vector<int> PresetBundle::get_default_nozzle_volume_types_for_filaments(std::vector<int>& f_maps)
{
std::vector<int> result;
int filament_count = f_maps.size();
result.resize(filament_count, static_cast<int>(NozzleVolumeType::nvtStandard));
auto opt_nozzle_volume_type = dynamic_cast<const ConfigOptionEnumsGeneric*>(this->project_config.option("nozzle_volume_type"));
for (int index = 0; index < filament_count; index++)
{
if (opt_nozzle_volume_type && opt_nozzle_volume_type->values.size() > (f_maps[index] - 1))
result[index] = opt_nozzle_volume_type->values[f_maps[index] - 1];
}
return result;
}
DynamicPrintConfig PresetBundle::full_config(bool apply_extruder, std::optional<std::vector<int>>filament_maps, std::optional<std::vector<int>> filament_volume_maps) const
{
return (this->printers.get_edited_preset().printer_technology() == ptFFF) ?
this->full_fff_config(apply_extruder, filament_maps) :
this->full_fff_config(apply_extruder, filament_maps, filament_volume_maps) :
this->full_sla_config();
}
@@ -3921,16 +4015,52 @@ DynamicPrintConfig PresetBundle::full_config_secure(std::optional<std::vector<in
config.erase("printhost_cafile");
config.erase("printhost_user");
config.erase("printhost_password");
config.erase("printhost_port");
config.erase("printhost_port");
return config;
}
std::vector<std::vector<std::vector<float>>> PresetBundle::get_full_flush_matrix(bool with_multiplier) const
{
auto full_config = this->full_config();
int extruder_nums = full_config.option<ConfigOptionFloats>("nozzle_diameter")->values.size();
std::vector<double> flush_volume_value = full_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
int filament_nums = full_config.option<ConfigOptionStrings>("filament_type")->values.size();
std::vector<std::vector<std::vector<float>>> matrix;
for (size_t extruder_id = 0; extruder_id < extruder_nums; ++extruder_id) {
std::vector<float> flush_matrix(cast<float>(get_flush_volumes_matrix(flush_volume_value, extruder_id, extruder_nums)));
std::vector<std::vector<float>> wipe_volumes;
for (unsigned int i = 0; i < filament_nums; ++i)
wipe_volumes.push_back(std::vector<float>(flush_matrix.begin() + i * filament_nums, flush_matrix.begin() + (i + 1) * filament_nums));
matrix.emplace_back(wipe_volumes);
}
if (with_multiplier) {
// Fast purge mode uses flush_multiplier_fast; the default prime_volume_mode==Default
// (or the key absent) reads flush_multiplier, so this is inert.
auto* mode_opt = project_config.option<ConfigOptionEnum<PrimeVolumeMode>>("prime_volume_mode");
const bool use_fast = mode_opt && mode_opt->value == PrimeVolumeMode::pvmFast;
auto* mult_opt = project_config.option<ConfigOptionFloats>(use_fast ? "flush_multiplier_fast" : "flush_multiplier");
auto flush_multiplies = mult_opt ? mult_opt->values : project_config.option<ConfigOptionFloats>("flush_multiplier")->values;
flush_multiplies.resize(extruder_nums, 1);
for (size_t extruder_id = 0; extruder_id < extruder_nums; ++extruder_id) {
for (auto& vec : matrix[extruder_id]) {
for (auto& v : vec)
v *= flush_multiplies[extruder_id];
}
}
}
return matrix;
}
const std::set<std::string> ignore_settings_list ={
"inherits",
"print_settings_id", "filament_settings_id", "printer_settings_id"
};
DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional<std::vector<int>> filament_maps_new) const
DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional<std::vector<int>> filament_maps_new, std::optional<std::vector<int>> filament_volume_maps_new) const
{
DynamicPrintConfig out;
out.apply(FullPrintConfig::defaults());
@@ -3944,8 +4074,17 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio
size_t num_filaments = this->filament_presets.size();
std::vector<int> filament_maps = out.option<ConfigOptionInts>("filament_map")->values;
std::vector<int> filament_volume_maps(num_filaments, (int)nvtStandard);
ConfigOptionInts* filament_volume_map_opt = out.option<ConfigOptionInts>("filament_volume_map");
if (filament_maps_new.has_value())
filament_maps = *filament_maps_new;
if (filament_volume_maps_new.has_value()) {
filament_volume_maps = *filament_volume_maps_new;
out.option<ConfigOptionInts>("filament_volume_map", true)->values = filament_volume_maps;
}
else if (filament_volume_map_opt && filament_volume_map_opt->values.size() == num_filaments)
filament_volume_maps = filament_volume_map_opt->values;
//in some middle state, they may be different
if (filament_maps.size() != num_filaments) {
filament_maps.resize(num_filaments, 1);
@@ -3953,6 +4092,9 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio
else {
assert(filament_maps.size() == num_filaments);
}
if (filament_volume_maps.size() != num_filaments) {
filament_volume_maps.resize(num_filaments, nvtStandard);
}
auto* extruder_diameter = dynamic_cast<const ConfigOptionFloats*>(out.option("nozzle_diameter"));
// Collect the "compatible_printers_condition" and "inherits" values over all presets (print, filaments, printers) into a single vector.
@@ -3982,18 +4124,34 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio
different_settings.emplace_back(different_print_settings);
//BBS: update printer config related with variants
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
int extruder_count = 1, extruder_volume_type_count = 1;
bool different_extruder = false;
if (apply_extruder) {
out.update_values_to_printer_extruders(out, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant");
out.update_values_to_printer_extruders(out, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2);
//update print config related with variants
out.update_values_to_printer_extruders(out, print_options_with_variant, "print_extruder_id", "print_extruder_variant");
different_extruder = out.support_different_extruders(extruder_count);
extruder_volume_type_count = out.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
if ((extruder_count > 1) || different_extruder) {
// Orca: keep processing variant_1 before variant_2 here; variant_2 slots are resolved
// against the printer id/variant lists as rewritten by the variant_1 pass, and the
// composed values depend on that order. Note the order is load-bearing, not correct
// in general: the variant_2 pass reads the original full-width arrays through indices
// resolved on the shrunk lists, which mis-reads presets whose variant_2 columns differ
// per variant (e.g. X2D machine_max_speed_e/machine_max_acceleration_e). The slicing
// path composes variant_2 first and is unaffected; changing the order here would alter
// long-standing composed values, so any fix must re-baseline them.
out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant");
out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2);
//update print config related with variants
out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant");
}
}
if (num_filaments <= 1) {
//BBS: update filament config related with variants
DynamicPrintConfig filament_config = this->filaments.get_edited_preset().config;
if (apply_extruder)
filament_config.update_values_to_printer_extruders(out, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0]);
if (apply_extruder && ((extruder_count > 1) || different_extruder))
filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]);
out.apply(filament_config);
compatible_printers_condition.emplace_back(this->filaments.get_edited_preset().compatible_printers_condition());
compatible_prints_condition .emplace_back(this->filaments.get_edited_preset().compatible_prints_condition());
@@ -4086,8 +4244,8 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio
filament_temp_configs.resize(num_filaments);
for (size_t i = 0; i < num_filaments; ++i) {
filament_temp_configs[i] = *(filament_configs[i]);
if (apply_extruder)
filament_temp_configs[i].update_values_to_printer_extruders(out, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i]);
if (apply_extruder && ((extruder_count > 1) || different_extruder))
filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]);
}
// loop through options and apply them to the resulting config.
@@ -4331,6 +4489,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
};
clear_compatible_printers(config);
// Dynamic per-nozzle filament mapping reflects live device state, not a stored setting;
// drop it from any imported config so it only comes from the connected printer.
config.erase("enable_filament_dynamic_map");
#if 0
size_t num_extruders = (printer_technology == ptFFF) ?
std::min(config.option<ConfigOptionFloats>("nozzle_diameter" )->values.size(),
@@ -4819,6 +4981,8 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
model.use_double_extruder_default_texture = it.value();
} else if (boost::iequals(it.key(), BBL_JSON_KEY_BOTTOM_TEXTURE_RECT)) {
model.bottom_texture_rect = it.value();
} else if (boost::iequals(it.key(), BBL_JSON_KEY_BOTTOM_TEXTURE_RECT_LONGER)) {
model.bottom_texture_rect_longer = it.value();
} else if (boost::iequals(it.key(), BBL_JSON_KEY_MIDDLE_TEXTURE_RECT)) {
model.middle_texture_rect = it.value();
}
@@ -5577,7 +5741,7 @@ void PresetBundle::set_default_suppressed(bool default_suppressed)
printers.set_default_suppressed(default_suppressed);
}
bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes, bool check_references) const
bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes) const
{
if (m_errors != 0 || printers.m_errors != 0 || filaments.m_errors != 0 || prints.m_errors != 0)
return true;
@@ -5600,7 +5764,7 @@ bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes, bool check
if (check_duplicate_filament_subtypes && this->check_duplicate_filament_subtypes())
has_errors = true;
if (check_references && this->check_preset_references())
if (this->check_preset_references())
has_errors = true;
return has_errors;
+34 -6
View File
@@ -72,6 +72,25 @@ struct FilamentBaseInfo
bool is_support{ false };
bool is_system{ true };
int filament_printable = 3;
// filament_extruder_compatibility packs one compatibility level per extruder into a single
// 32-bit int, 3 bits per extruder (up to 10 extruders). Levels: 0 = printable, 1 = error,
// 2 = critical warning, 3 = warning (4-7 reserved). extruder_id is 0-based.
int get_extruder_compatibility(int extruder_id) const {
constexpr int bits_per_extruder = 3;
constexpr int extruder_mask = (1 << bits_per_extruder) - 1; // 0x7
constexpr int max_extruder_count = 32 / bits_per_extruder; // 10
if (extruder_id < 0 || extruder_id >= max_extruder_count)
return 0;
return (m_filament_extruder_compatibility >> (bits_per_extruder * extruder_id)) & extruder_mask;
}
void set_filament_extruder_compatibility(int value) { m_filament_extruder_compatibility = value; }
int get_filament_extruder_compatibility() const { return m_filament_extruder_compatibility; }
private:
int m_filament_extruder_compatibility = 0;
};
enum BundleType{
@@ -156,7 +175,8 @@ public:
const DynamicPrintConfig &project_config,
std::vector<Preset> &in_filament_presets,
bool apply_extruder,
std::optional<std::vector<int>> filament_maps_new);
std::optional<std::vector<int>> filament_maps_new,
std::optional<std::vector<int>> filament_volume_maps_new = std::nullopt);
// ORCA: utility function to find the vendor for a given preset name
static std::string find_preset_vendor(const std::string& preset_name, Preset::Type type);
@@ -364,10 +384,19 @@ public:
bool has_defauls_only() const
{ return prints.has_defaults_only() && filaments.has_defaults_only() && printers.has_defaults_only(); }
DynamicPrintConfig full_config(bool apply_extruder = true, std::optional<std::vector<int>>filament_maps = std::nullopt) const;
DynamicPrintConfig full_config(bool apply_extruder = true, std::optional<std::vector<int>>filament_maps = std::nullopt, std::optional<std::vector<int>> filament_volume_maps = std::nullopt) const;
// full_config() with the some "useless" config removed.
DynamicPrintConfig full_config_secure(std::optional<std::vector<int>>filament_maps = std::nullopt) const;
// Default per-filament nozzle-volume types: each filament inherits the volume type of the
// extruder it maps to (1-based f_maps), Standard when unknown.
std::vector<int> get_default_nozzle_volume_types_for_filaments(std::vector<int>& f_maps);
// Per-extruder flush matrix [extruder_id][from_filament][to_filament] in mm^3, optionally scaled
// by the per-extruder flush_multiplier (or flush_multiplier_fast when prime_volume_mode==Fast).
// Used by the print-dispatch nozzle-mapping flush-weight estimate.
std::vector<std::vector<std::vector<float>>> get_full_flush_matrix(bool with_multiplier = true) const;
//BBS: add some functions for multiple extruders
int get_printer_extruder_count() const;
bool support_different_extruders() const;
@@ -485,9 +514,8 @@ public:
return { Preset::TYPE_PRINTER, Preset::TYPE_SLA_PRINT, Preset::TYPE_SLA_MATERIAL };
}
// Orca: for validation only. The duplicate filament subtype and preset-reference checks are
// opt-in for now (enabled per-vendor by the profile-check CI as vendors are cleaned up).
bool has_errors(bool check_duplicate_filament_subtypes = false, bool check_preset_references = false) const;
// Orca: for validation only.
bool has_errors(bool check_duplicate_filament_subtypes = false) const;
// Orca: for validation only. Flag any system preset whose inherits / compatible_printers /
// compatible_prints references a deleted (unknown) or renamed (old) preset name.
@@ -520,7 +548,7 @@ private:
/*ConfigSubstitutions load_config_file_config_bundle(
const std::string &path, const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule);*/
DynamicPrintConfig full_fff_config(bool apply_extruder, std::optional<std::vector<int>> filament_maps=std::nullopt) const;
DynamicPrintConfig full_fff_config(bool apply_extruder, std::optional<std::vector<int>> filament_maps=std::nullopt, std::optional<std::vector<int>> filament_volume_maps=std::nullopt) const;
DynamicPrintConfig full_sla_config() const;
// Orca: used for validation only
+814 -146
View File
File diff suppressed because it is too large Load Diff
+168 -13
View File
@@ -17,12 +17,14 @@
#include "GCode/ThumbnailData.hpp"
#include "GCode/GCodeProcessor.hpp"
#include "MultiMaterialSegmentation.hpp"
#include "ObjectID.hpp"
#include "libslic3r.h"
#include <Eigen/Geometry>
#include <functional>
#include <set>
#include <unordered_map>
#include "calib.hpp"
@@ -38,6 +40,7 @@ class SupportLayer;
class TreeSupportData;
class TreeSupport;
class ExtrusionLayers;
namespace MultiNozzleUtils { class NozzleGroupResultBase; class LayeredNozzleGroupResult; }
#define MAX_OUTER_NOZZLE_DIAMETER 4
// BBS: move from PrintObjectSlice.cpp
@@ -99,6 +102,14 @@ enum PrintObjectStep {
posCount,
};
enum class SlicingPipelineStepPlugin {
posSlice, posPerimeters, posEstimateCurledExtrusions, posPrepareInfill, posInfill, posIroning, posContouring,
posSupportMaterial, posDetectOverhangsForLift, posSimplifyPath, psWipeTower, psSkirtBrim,
// Fires from the GUI G-code export/post-process seam (PostProcessor.cpp), NOT from Print::process().
// At this step the plugin edits the exported G-code file in place; see SlicingPipelinePluginCapability for the full contract.
psGCodePostProcess
};
// A PrintRegion object represents a group of volumes to print
// sharing the same config (including the same assigned extruder(s))
class PrintRegion
@@ -418,7 +429,7 @@ public:
// (layer height, first layer height, raft settings, print nozzle diameter etc).
const SlicingParameters& slicing_parameters() const { return m_slicing_params; }
// Orca: XYZ shrinkage compensation has introduced the const Vec3d &object_shrinkage_compensation parameter to the function below
static SlicingParameters slicing_parameters(const DynamicPrintConfig &full_config, const ModelObject &model_object, float object_max_z, const Vec3d &object_shrinkage_compensation);
static SlicingParameters slicing_parameters(const DynamicPrintConfig &full_config, const ModelObject &model_object, float object_max_z, const Vec3d &object_shrinkage_compensation, std::vector<int> variant_index = std::vector<int>());
size_t num_printing_regions() const throw() { return m_shared_regions->all_regions.size(); }
const PrintRegion& printing_region(size_t idx) const throw() { return *m_shared_regions->all_regions[idx].get(); }
@@ -489,7 +500,7 @@ public:
// If ! m_slicing_params.valid, recalculate.
void update_slicing_parameters();
static PrintObjectConfig object_config_from_model_object(const PrintObjectConfig &default_object_config, const ModelObject &object, size_t num_extruders);
static PrintObjectConfig object_config_from_model_object(const PrintObjectConfig &default_object_config, const ModelObject &object, size_t num_extruders, std::vector<int>& variant_index);
private:
void make_perimeters();
@@ -772,6 +783,7 @@ struct WipeTowerData
number_of_toolchanges = -1;
depth = 0.f;
brim_width = 0.f;
height = 0.f;
rib_offset = Vec2f::Zero();
wipe_tower_mesh_data = std::nullopt;
}
@@ -891,6 +903,11 @@ private: // Prevents erroneous use by other classes.
typedef std::pair<PrintObject *, bool> PrintObjectInfo;
public:
using SlicingPipelineHookFn = std::function<void(Print&, const PrintObject*, SlicingPipelineStepPlugin)>;
// Cross-layer injection (mirrors ConfigBase::set_resolve_capability_fn): the GUI/plugin
// layer registers a dispatcher; libslic3r stays free of any plugin/Python dependency.
static void set_slicing_pipeline_hook_fn(SlicingPipelineHookFn fn) { s_slicing_pipeline_hook_fn = std::move(fn); }
Print() = default;
virtual ~Print() { this->clear(); }
@@ -958,7 +975,7 @@ public:
[object_id](const PrintObject *obj) { return obj->id() == object_id; });
return (it == m_objects.end()) ? nullptr : *it;
}
//BBS: Function to get m_brimMap;
// Orca: Old callers still expect object-keyed brim paths.
std::map<ObjectID, ExtrusionEntityCollection>&
get_brimMap() { return m_brimMap; }
@@ -973,11 +990,11 @@ public:
struct SkirtBrimGroup {
struct Brim {
ExtrusionEntityCollection brim;
std::vector<ObjectID> object_ids;
std::vector<ObjectInstanceID> instances;
};
ExtrusionEntityCollection skirt;
std::vector<ObjectID> object_ids;
std::vector<ObjectInstanceID> instances;
// Brims stay separate unless Combine brims merges colliding brims inside this group.
std::vector<Brim> brims;
};
@@ -1003,15 +1020,50 @@ public:
const WipeTowerData& wipe_tower_data(size_t filaments_cnt = 0) const;
const ToolOrdering& tool_ordering() const { return m_tool_ordering; }
void update_filament_maps_to_config(std::vector<int> f_maps);
void update_filament_maps_to_config(std::vector<int> f_maps, std::vector<int> f_volume_maps = std::vector<int>{}, std::vector<int> f_nozzle_maps = std::vector<int>{});
// Write-back for a selector (per-layer planned) grouping result. When a filament actually
// migrates between nozzle variants, rebuilds the per-slot filament arrays so it holds one
// slot per variant and recomputes the extruder retract overrides against the expanded
// slots — update_filament_maps_to_config's single-slot rebuild cannot represent a
// migration. A result without migration reduces to a single grouping and takes the
// three-map write-back like the static paths.
void update_to_config_by_nozzle_group_result(const MultiNozzleUtils::LayeredNozzleGroupResult& group_result);
void apply_config_for_render(const DynamicConfig &config);
// 1 based group ids
std::vector<int> get_filament_maps() const;
FilamentMapMode get_filament_map_mode() const;
std::vector<int> get_filament_volume_maps() const;
std::vector<int> get_filament_nozzle_maps() const;
// get the group label of filament
size_t get_extruder_id(unsigned int filament_id) const;
// The region every extruder can reach,
// i.e. the intersection of all per-extruder printable areas. Falls back to the full printable_area
// for single-nozzle printers and whenever extruder_printable_area is not populated (all current
// single/dual profiles), so the wipe-tower-center clamp is byte-identical to full-bed clamping there.
Polygons get_extruder_shared_printable_polygon() const;
// Logical (extruder, nozzle) grouping result produced by ToolOrdering during reorder.
// Consumed by GCode via get_layered_nozzle_group_result()->get_nozzle_id(filament, layer) etc.
void set_nozzle_group_result(std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> result) { m_nozzle_group_result = result; }
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> get_nozzle_group_result() const { return m_nozzle_group_result; }
std::shared_ptr<MultiNozzleUtils::LayeredNozzleGroupResult> get_layered_nozzle_group_result() const;
// True only when the project opts into the per-layer filament selector
// (enable_filament_dynamic_map) in auto-for-flush mode on a multi-extruder machine. Gates the
// dynamic (per-layer) regroup branch in ToolOrdering::reorder_extruders_for_minimum_flush_volume,
// the sequential (by-object) plan stitching in Print::process, and GCode's use of the cached
// sequential plans. No profile sets the flag, so the static grouping path (byte-identical
// output) is the only one taken unless the user enables the selector.
bool is_dynamic_group_reorder() const;
// Per-object tool orderings planned by the sequential (by-object) selector regroup with
// cross-object nozzle-status threading. GCode export must consume these exact plans: a fresh
// per-object construction would re-plan from a different seed and diverge from the published
// stitched result. Empty on the static path.
const std::map<const PrintObject*, ToolOrdering>& sequential_dynamic_orderings() const { return m_sequential_dynamic_orderings; }
const std::vector<std::vector<DynamicPrintConfig>>& get_extruder_filament_info() const { return m_extruder_filament_info; }
void set_extruder_filament_info(const std::vector<std::vector<DynamicPrintConfig>>& filament_info) { m_extruder_filament_info = filament_info; }
@@ -1037,6 +1089,18 @@ public:
*/
std::vector<std::set<int>> get_physical_unprintable_filaments(const std::vector<unsigned int>& used_filaments) const;
/**
* @brief Determines the forbidden nozzle volume types for each used filament
*
* A filament may declare the extruder variants it supports. Every volume type offered by the
* printer's extruders that the filament does not support is forbidden for that filament.
* Hybrid volumes are ignored on both sides, and filaments declaring no variants are unrestricted.
*
* @param used_filaments Totally used filaments when slicing
* @return A map from used filament index to the set of nozzle volume types it cannot print on
*/
std::map<int, std::set<NozzleVolumeType>> get_filament_unprintable_flow(const std::vector<unsigned int> &used_filaments) const;
std::vector<double> get_extruder_printable_height() const;
std::vector<Polygons> get_extruder_printable_polygons() const;
std::vector<Polygons> get_extruder_unprintable_polygons() const;
@@ -1120,7 +1184,12 @@ public:
bool is_all_objects_are_short() const {
return std::all_of(this->objects().begin(), this->objects().end(), [&](PrintObject* obj) { return obj->height() < scale_(this->config().nozzle_height.value); });
}
// Post-slicing config-slot resolvers: map a (filament, layer) pair to the index of its
// per-(extruder x volume type) column in the expanded variant arrays, cached by grouping context.
int get_filament_config_indx(int filament_id, int layer_id);
int get_nozzle_config_index(int filament_id, int layer_id);
// Orca: Implement prusa's filament shrink compensation approach
// Returns if all used filaments have same shrinkage compensations.
bool has_same_shrinkage_compensations() const;
@@ -1130,6 +1199,57 @@ public:
std::tuple<float, float> object_skirt_offset(double margin_height = 0) const;
protected:
struct FilamentIndexKey
{
int filament_id;
ExtruderType extruder;
NozzleVolumeType nozzle_volume_type;
bool operator==(const FilamentIndexKey &other) const
{
return filament_id == other.filament_id && extruder == other.extruder && nozzle_volume_type == other.nozzle_volume_type;
}
};
struct PrintIndexKey
{
int filament_id;
int extruder_id;
ExtruderType extruder;
NozzleVolumeType nozzle_volume_type;
bool operator==(const PrintIndexKey &other) const
{
return filament_id == other.filament_id && extruder_id == other.extruder_id && extruder == other.extruder && nozzle_volume_type == other.nozzle_volume_type;
}
};
struct FilamentIndexKeyHash
{
std::size_t operator()(const FilamentIndexKey &k) const
{
size_t h1 = std::hash<int>{}(k.filament_id);
size_t h2 = std::hash<int>{}(static_cast<int>(k.extruder));
size_t h3 = std::hash<int>{}(static_cast<int>(k.nozzle_volume_type));
return h1 ^ (h2 << 8) ^ (h3 << 12);
}
};
struct PrintIndexKeyHash
{
std::size_t operator()(const PrintIndexKey &k) const
{
size_t h1 = std::hash<int>{}(k.filament_id);
size_t h2 = std::hash<int>{}(k.extruder_id);
size_t h3 = std::hash<int>{}(static_cast<int>(k.extruder));
size_t h4 = std::hash<int>{}(static_cast<int>(k.nozzle_volume_type));
return h1 ^ (h2 << 8) ^ (h3 << 12) ^ (h4 << 16);
}
};
using FilamentIndexMap = std::unordered_map<FilamentIndexKey, int, FilamentIndexKeyHash>;
using PrintIndexMap = std::unordered_map<PrintIndexKey, int, PrintIndexKeyHash>;
int get_config_index(int filament_id, int layer_id, const std::vector<std::string> &variant_list, const std::vector<int>& self_index_list, FilamentIndexMap &index_map);
int get_config_index(int filament_id, int layer_id, const std::vector<std::string> &variant_list, const std::vector<int>& self_index_list, PrintIndexMap &index_map);
// Invalidates the step, and its depending steps in Print.
bool invalidate_step(PrintStep step);
@@ -1143,10 +1263,27 @@ private:
void _make_skirt();
void _make_wipe_tower();
void finalize_first_layer_convex_hull();
void update_filament_self_index_cache();
// Deduplicates, per filament, the (extruder type x volume type) variants the grouping
// result routes it through; filaments the plan never routes get their default-map
// assignment so the slot resolution never depends on the (mutable) filament_map. config
// must carry extruder_type; returns false when it does not. Both the slice-time write-back
// and the apply-time reproduction call this with m_ori_full_print_config so the two
// expansions resolve identical slots.
bool collect_filament_variant_uses(const MultiNozzleUtils::LayeredNozzleGroupResult& group_result,
const DynamicPrintConfig& config,
std::unordered_map<int, std::vector<FilamentVariantUse>>& uses) const;
// Islands of objects and their supports extruded at the 1st layer.
Polygons first_layer_islands() const;
static SlicingPipelineHookFn s_slicing_pipeline_hook_fn;
bool m_pipeline_plugin_active { false };
void run_pipeline_hook(SlicingPipelineStepPlugin step, const PrintObject* object) {
if (m_pipeline_plugin_active && s_slicing_pipeline_hook_fn)
s_slicing_pipeline_hook_fn(*this, object, step);
}
PrintConfig m_config;
PrintObjectConfig m_default_object_config;
PrintRegionConfig m_default_region_config;
@@ -1154,18 +1291,18 @@ private:
PrintRegionPtrs m_print_regions;
//SoftFever
bool m_isBBLPrinter;
bool m_isBBLPrinter = false;
// Ordered collections of extrusion paths to build skirt loops and brim.
ExtrusionEntityCollection m_skirt;
std::vector<SkirtBrimGroup> m_skirt_brim_groups;
bool m_has_shared_per_object_skirt { false };
// BBS: collecting extrusion paths to build brim by objs
// Orca: Object-keyed brim paths kept for existing code.
std::map<ObjectID, ExtrusionEntityCollection> m_brimMap;
std::map<ObjectID, ExtrusionEntityCollection> m_supportBrimMap;
// Orca: cached occupied brim footprints used when grouping per-object skirts.
std::map<ObjectID, ExPolygons> m_objectBrimAreas;
std::map<ObjectID, ExPolygons> m_supportBrimAreas;
// Orca: Actual brim paths keyed by object instance.
std::map<ObjectInstanceID, ExtrusionEntityCollection> m_brimMapByInstance;
// Orca: Translated brim areas keyed by instance, used to find touching brims.
std::map<ObjectInstanceID, ExPolygons> m_objectBrimAreasByInstance;
// Convex hull of the 1st layer extrusions.
// It encompasses the object extrusions, support extrusions, skirt, brim, wipe tower.
// It does NOT encompass user extrusions generated by custom G-code,
@@ -1176,6 +1313,24 @@ private:
std::vector<std::vector<DynamicPrintConfig>> m_extruder_filament_info;
// Logical (extruder, nozzle) grouping result, set by ToolOrdering during reorder.
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> m_nozzle_group_result;
// Sequential (by-object) selector plans, keyed by object; see sequential_dynamic_orderings().
// Rebuilt (or cleared) on every process().
std::map<const PrintObject*, ToolOrdering> m_sequential_dynamic_orderings;
// Used to cache filament parameter information
FilamentIndexMap m_filament_index_map;
// Used to cache printer and process parameter information
PrintIndexMap m_nozzle_index_map;
// Orca: filament ids already reported as missing a nozzle-group entry this slice. get_config_index()
// falls back per-filament/per-layer in the g-code hot path, so this dedupes its log to once per
// filament instead of flooding thousands of identical error lines. Cleared with the caches each slice.
std::set<int> m_missing_nozzle_group_logged;
// save the config value of "filament_self_index"
std::vector<int> m_filament_self_index;
// Following section will be consumed by the GCodeGenerator.
ToolOrdering m_tool_ordering;
WipeTowerData m_wipe_tower_data {m_tool_ordering};
+179 -32
View File
@@ -224,7 +224,11 @@ static t_config_option_keys print_config_diffs(
const DynamicPrintConfig &new_full_config,
DynamicPrintConfig &filament_overrides,
int plate_index,
std::vector<int>& filament_maps)
std::vector<int>& filament_maps,
// Per-slot machine indices when the filament arrays hold the per-variant expansion of a
// selector result (one slot per variant a filament migrates through); the per-filament
// map cannot index the expanded override arrays. Null on the single-slot path.
const std::vector<int>* dynamic_override_indices = nullptr)
{
const std::vector<std::string> &extruder_retract_keys = print_config_def.extruder_retract_keys();
const std::string filament_prefix = "filament_";
@@ -240,7 +244,15 @@ static t_config_option_keys print_config_diffs(
const ConfigOption *opt_new_filament = std::binary_search(extruder_retract_keys.begin(), extruder_retract_keys.end(), opt_key) ? new_full_config.option(filament_prefix + opt_key) : nullptr;
if (opt_new_filament != nullptr) {
compute_filament_override_value(opt_key, opt_old, opt_new, opt_new_filament, new_full_config, print_diff, filament_overrides, filament_maps);
std::vector<int> filament_map_indices;
if (dynamic_override_indices)
filament_map_indices = *dynamic_override_indices;
else {
filament_map_indices.assign(filament_maps.size(), 0);
for (int i = 0; i < filament_maps.size(); i++)
filament_map_indices[i] = filament_maps[i] - 1;
}
compute_filament_override_value(opt_key, opt_old, opt_new, opt_new_filament, new_full_config, print_diff, filament_overrides, filament_map_indices);
} else if (*opt_new != *opt_old) {
//BBS: add plate_index logic for wipe_tower_x/wipe_tower_y
if (!opt_key.compare("wipe_tower_x") || !opt_key.compare("wipe_tower_y")) {
@@ -724,7 +736,7 @@ PrintObjectRegions::BoundingBox find_modifier_volume_extents(const PrintObjectRe
return out;
}
PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &default_or_parent_region_config, const DynamicPrintConfig *layer_range_config, const ModelVolume &volume, size_t num_extruders);
PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &default_or_parent_region_config, const DynamicPrintConfig *layer_range_config, const ModelVolume &volume, size_t num_extruders, std::vector<int>& variant_index);
void print_region_ref_inc(PrintRegion &r) { ++ r.m_ref_cnt; }
void print_region_ref_reset(PrintRegion &r) { r.m_ref_cnt = 0; }
@@ -738,7 +750,8 @@ bool verify_update_print_object_regions(
const PrintRegionConfig &default_region_config,
size_t num_extruders,
PrintObjectRegions &print_object_regions,
const std::function<void(const PrintRegionConfig&, const PrintRegionConfig&, const t_config_option_keys&)> &callback_invalidate)
const std::function<void(const PrintRegionConfig&, const PrintRegionConfig&, const t_config_option_keys&)> &callback_invalidate,
std::vector<int>& variant_index)
{
// Sort by ModelVolume ID.
model_volumes_sort_by_id(model_volumes);
@@ -783,7 +796,7 @@ bool verify_update_print_object_regions(
} else if (PrintObjectRegions::BoundingBox parent_bbox = find_modifier_volume_extents(layer_range, parent_region_id); parent_bbox.intersects(*bbox))
// Such parent region does not exist. If it is needed, then we need to reslice.
// Only create new region for a modifier, which actually modifies config of it's parent.
if (PrintRegionConfig config = region_config_from_model_volume(parent_region.region->config(), nullptr, **it_model_volume, num_extruders);
if (PrintRegionConfig config = region_config_from_model_volume(parent_region.region->config(), nullptr, **it_model_volume, num_extruders, variant_index);
config != parent_region.region->config())
// This modifier newly overrides a region, which it did not before. We need to reslice.
return false;
@@ -791,8 +804,8 @@ bool verify_update_print_object_regions(
}
}
PrintRegionConfig cfg = region.parent == -1 ?
region_config_from_model_volume(default_region_config, layer_range.config, **it_model_volume, num_extruders) :
region_config_from_model_volume(layer_range.volume_regions[region.parent].region->config(), nullptr, **it_model_volume, num_extruders);
region_config_from_model_volume(default_region_config, layer_range.config, **it_model_volume, num_extruders, variant_index) :
region_config_from_model_volume(layer_range.volume_regions[region.parent].region->config(), nullptr, **it_model_volume, num_extruders, variant_index);
if (cfg != region.region->config()) {
// Region configuration changed.
if (print_region_ref_cnt(*region.region) == 0) {
@@ -964,6 +977,7 @@ static PrintObjectRegions* generate_print_object_regions(
size_t num_extruders,
const float xy_contour_compensation,
const std::vector<unsigned int> &painting_extruders,
std::vector<int> &variant_index,
const bool has_painted_fuzzy_skin)
{
// Reuse the old object or generate a new one.
@@ -1022,7 +1036,7 @@ static PrintObjectRegions* generate_print_object_regions(
// Add a model volume, assign an existing region or generate a new one.
layer_range.volume_regions.push_back({
&volume, -1,
get_create_region(region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders)),
get_create_region(region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders, variant_index)),
bbox
});
} else if (volume.is_negative_volume()) {
@@ -1039,7 +1053,7 @@ static PrintObjectRegions* generate_print_object_regions(
if (parent_volume.is_model_part() || parent_volume.is_modifier())
if (PrintObjectRegions::BoundingBox parent_bbox = find_modifier_volume_extents(layer_range, parent_region_id); parent_bbox.intersects(*bbox)) {
// Only create new region for a modifier, which actually modifies config of it's parent.
if (PrintRegionConfig config = region_config_from_model_volume(parent_region.region->config(), nullptr, volume, num_extruders);
if (PrintRegionConfig config = region_config_from_model_volume(parent_region.region->config(), nullptr, volume, num_extruders, variant_index);
config != parent_region.region->config()) {
added = true;
layer_range.volume_regions.push_back({ &volume, parent_region_id, get_create_region(std::move(config)), bbox });
@@ -1162,25 +1176,58 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
}
//apply extruder related values
std::vector<int> print_variant_index;
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
int extruder_count = 1, extruder_volume_type_count = 1;
bool different_extruder = false;
// Filled only when the filament arrays are rebuilt from a persisted selector result below;
// print_config_diffs then keys the retract overrides per expanded slot.
std::vector<int> dynamic_slot_indices;
different_extruder = new_full_config.support_different_extruders(extruder_count);
extruder_volume_type_count = new_full_config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
if (!extruder_applied) {
// variant_2 must be processed first, because variant_1 will make `printer_extruder_id` and `printer_extruder_variant` half of the size that makes `get_index_for_extruder` no longer work properly
new_full_config.update_values_to_printer_extruders(new_full_config, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2);
new_full_config.update_values_to_printer_extruders(new_full_config, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant");
//update print config related with variants
new_full_config.update_values_to_printer_extruders(new_full_config, print_options_with_variant, "print_extruder_id", "print_extruder_variant");
if ((extruder_count > 1) || different_extruder) {
// variant_2 must be processed first, because variant_1 will make `printer_extruder_id` and `printer_extruder_variant` half of the size that makes `get_index_for_extruder` no longer work properly
new_full_config.update_values_to_printer_extruders(new_full_config, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2);
new_full_config.update_values_to_printer_extruders(new_full_config, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant");
//update print config related with variants
print_variant_index = new_full_config.update_values_to_printer_extruders(new_full_config, extruder_count, extruder_volume_type_count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant");
}
else
print_variant_index.resize(1, 0);
m_ori_full_print_config = new_full_config;
new_full_config.update_values_to_printer_extruders_for_multiple_filaments(new_full_config, filament_options_with_variant, "filament_self_index", "filament_extruder_variant");
std::set<std::string> filament_keys = filament_options_with_variant;
filament_keys.insert("filament_self_index");
// A persisted selector result with an actual migration means the last slice rebuilt the
// per-slot filament arrays from it (one slot per variant a filament prints through).
// Reproduce that exact expansion here so an unchanged config diffs empty — the expanded
// keys invalidate the wipe tower / g-code export, and the placeholder parser aliases
// the full config — instead of trimming back to one slot per filament.
auto group_result = std::dynamic_pointer_cast<MultiNozzleUtils::LayeredNozzleGroupResult>(this->get_nozzle_group_result());
std::unordered_map<int, std::vector<FilamentVariantUse>> filament_variant_uses;
if (group_result && group_result->is_support_dynamic_nozzle_map()
&& collect_filament_variant_uses(*group_result, m_ori_full_print_config, filament_variant_uses))
new_full_config.update_filament_config_values_for_multiple_extruders(m_ori_full_print_config, filament_variant_uses,
extruder_count, extruder_volume_type_count, filament_keys,
"filament_self_index", "filament_extruder_variant",
&dynamic_slot_indices);
else if ((extruder_count > 1) || different_extruder)
new_full_config.update_values_to_printer_extruders_for_multiple_filaments(m_ori_full_print_config, extruder_count, extruder_volume_type_count, filament_keys,
"filament_self_index", "filament_extruder_variant");
}
else {
//should not come here, we can not get the result of print_variant, for the values have been updated
//we just use the default values here
auto variant_opt = dynamic_cast<const ConfigOptionStrings *>(new_full_config.option("printer_extruder_variant"));
print_variant_index.resize(variant_opt->values.size());
for (int e_index = 0; e_index < variant_opt->values.size(); e_index++)
{
print_variant_index[e_index] = e_index;
}
}
// else {
// int extruder_count;
// bool different_extruder = new_full_config.support_different_extruders(extruder_count);
// print_variant_index.resize(extruder_count);
// for (int e_index = 0; e_index < extruder_count; e_index++)
// {
// print_variant_index[e_index] = e_index;
// }
// }
auto opt_filament_map = new_full_config.option<ConfigOptionInts>("filament_map");
std::vector<int> filament_maps = opt_filament_map ? opt_filament_map->values : std::vector<int>();
@@ -1188,7 +1235,15 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// Find modified keys of the various configs. Resolve overrides extruder retract values by filament profiles.
DynamicPrintConfig filament_overrides;
//BBS: add plate index
t_config_option_keys print_diff = print_config_diffs(m_config, new_full_config, filament_overrides, this->m_plate_index, filament_maps);
t_config_option_keys print_diff = print_config_diffs(m_config, new_full_config, filament_overrides, this->m_plate_index, filament_maps,
dynamic_slot_indices.empty() ? nullptr : &dynamic_slot_indices);
// Orca: filament_map_2 is engine-derived state, never a user input: the rebuild below
// recomputes it from filament_map/filament_volume_map/the variant slots on every apply
// (all of which are diffed and invalidation-listed on their own), and the grouping
// write-back overwrites it during process(). The incoming full config only ever carries
// the ConfigDef default, so diffing it would invalidate every print step on each apply
// for any multi-extruder printer and permanently invalidate fresh slice results.
print_diff.erase(std::remove(print_diff.begin(), print_diff.end(), "filament_map_2"), print_diff.end());
t_config_option_keys full_config_diff = full_print_config_diffs(m_full_print_config, new_full_config, this->m_plate_index);
// Collect changes to object and region configs.
t_config_option_keys object_diff = m_default_object_config.diff(new_full_config);
@@ -1196,10 +1251,10 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
//BBS: process the filament_map related logic
std::unordered_set<std::string> print_diff_set(print_diff.begin(), print_diff.end());
if (print_diff_set.find("filament_map_mode") == print_diff_set.end())
if (!print_diff_set.empty() && print_diff_set.find("filament_map_mode") == print_diff_set.end())
{
FilamentMapMode map_mode = new_full_config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value;
if (map_mode < fmmManual) {
if (is_auto_filament_map_mode(map_mode)) {
if (print_diff_set.find("filament_map") != print_diff_set.end()) {
print_diff_set.erase("filament_map");
//full_config_diff.erase("filament_map");
@@ -1208,9 +1263,29 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
old_opt->set(new_opt);
m_config.filament_map = *new_opt;
}
if (print_diff_set.find("filament_volume_map") != print_diff_set.end()) {
print_diff_set.erase("filament_volume_map");
//full_config_diff.erase("filament_volume_map");
ConfigOptionInts* old_opt = m_full_print_config.option<ConfigOptionInts>("filament_volume_map", true);
ConfigOptionInts* new_opt = new_full_config.option<ConfigOptionInts>("filament_volume_map", true);
old_opt->set(new_opt);
m_config.filament_volume_map = *new_opt;
}
if (print_diff_set.find("filament_nozzle_map") != print_diff_set.end()) {
print_diff_set.erase("filament_nozzle_map");
//full_config_diff.erase("filament_nozzle_map");
ConfigOptionInts* old_opt = m_full_print_config.option<ConfigOptionInts>("filament_nozzle_map", true);
ConfigOptionInts* new_opt = new_full_config.option<ConfigOptionInts>("filament_nozzle_map", true);
old_opt->set(new_opt);
m_config.filament_nozzle_map = *new_opt;
}
}
else {
print_diff_set.erase("extruder_ams_count");
if (map_mode == fmmManual) {
// filament_nozzle_map is an engine output, not a GUI input, in manual mode
print_diff_set.erase("filament_nozzle_map");
}
std::vector<int> old_filament_map = m_config.filament_map.values;
std::vector<int> new_filament_map = new_full_config.option<ConfigOptionInts>("filament_map", true)->values;
@@ -1227,14 +1302,66 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
break;
}
}
if (same_map)
if (same_map) {
print_diff_set.erase("filament_map");
// The extruder retract overrides are keyed by the (unchanged) filament map;
// recompute them and drop diffs whose recomputed value matches the current
// config, so a cosmetic reordering of unused filaments does not invalidate.
const auto& retract_keys = print_config_def.extruder_retract_keys();
const std::string filament_prefix = "filament_";
std::vector<int> old_f_map_indices(old_filament_map.size(), 0);
for (size_t i = 0; i < old_filament_map.size(); i++)
old_f_map_indices[i] = old_filament_map[i] - 1;
for (const auto& rk : retract_keys) {
if (print_diff_set.find(rk) == print_diff_set.end())
continue;
const ConfigOption* opt_old = m_config.option(rk);
const ConfigOption* opt_new_m = new_full_config.option(rk);
const ConfigOption* opt_new_f = new_full_config.option(filament_prefix + rk);
if (opt_old && opt_new_m && opt_new_f) {
std::unique_ptr<ConfigOption> opt_recomputed(opt_new_m->clone());
opt_recomputed->apply_override(opt_new_f, old_f_map_indices);
if (*opt_old == *opt_recomputed)
print_diff_set.erase(rk);
}
}
}
}
}
if (print_diff_set.size() != print_diff.size())
print_diff.assign(print_diff_set.begin(), print_diff_set.end());
}
//filament_map_2
// Orca: seed with 0-based extruder indices so the copy stays a valid slot map even when the
// variant options are absent below and the rebuild loop is skipped (unit tests, degenerate
// presets); the loop overwrites every entry when it runs.
m_config.filament_map_2.values = filament_maps;
for (auto& v : m_config.filament_map_2.values)
--v;
auto opt_extruder_type = dynamic_cast<const ConfigOptionEnumsGeneric*>(new_full_config.option("extruder_type"));
auto opt_filament_volume_maps = dynamic_cast<const ConfigOptionInts*>(new_full_config.option("filament_volume_map"));
auto opt_nozzle_volume_type = dynamic_cast<const ConfigOptionEnumsGeneric*>(new_full_config.option("nozzle_volume_type"));
for (int index = 0; opt_extruder_type && opt_nozzle_volume_type && index < filament_maps.size(); index++)
{
ExtruderType extruder_type = (ExtruderType)(opt_extruder_type->get_at(filament_maps[index] - 1));
NozzleVolumeType nozzle_volume_type = (NozzleVolumeType)(opt_nozzle_volume_type->get_at(filament_maps[index] - 1));
// Orca: honour the per-filament volume map only when a producer sized it to the filament
// count; mis-sized maps (stale project values, CLI runs until the per-filament synthesis
// lands there) must not be indexed per filament (see
// update_values_to_printer_extruders_for_multiple_filaments for the same guard).
if ((extruder_volume_type_count > extruder_count) && opt_filament_volume_maps
&& opt_filament_volume_maps->values.size() == filament_maps.size())
nozzle_volume_type = (NozzleVolumeType)(opt_filament_volume_maps->values[index]);
// Orca: when the process variant columns cannot be matched (degenerate
// print_extruder_id), key the override by plain extruder index like the seeding
// above instead of poisoning the map with -1.
int slot_index = new_full_config.get_index_for_extruder(filament_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant");
m_config.filament_map_2.values[index] = slot_index >= 0 ? slot_index : filament_maps[index] - 1;
}
// Do not use the ApplyStatus as we will use the max function when updating apply_status.
unsigned int apply_status = APPLY_STATUS_UNCHANGED;
auto update_apply_status = [&apply_status](bool invalidated)
@@ -1282,11 +1409,22 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
m_default_region_config.apply_only(new_full_config, region_diff, true);
//m_full_print_config = std::move(new_full_config);
m_full_print_config = new_full_config;
update_filament_self_index_cache();
if (num_extruders != m_config.filament_diameter.size()) {
num_extruders = m_config.filament_diameter.size();
num_extruders_changed = true;
}
}
else if (! print_diff.empty()) {
// Orca: m_config can diverge from an unchanged full config (e.g. the in-slice retract
// override recompute writing different values than the apply-time computation). The
// invalidation above already fired for print_diff, so repair m_config here as well;
// otherwise the divergence is never corrected and every subsequent apply of the same
// config invalidates the result again, forever.
m_placeholder_parser.apply_config(filament_overrides);
m_config.apply_only(new_full_config, print_diff, true);
m_config.apply(filament_overrides);
}
ModelObjectStatusDB model_object_status_db;
@@ -1475,7 +1613,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
if (object_config_changed)
model_object.config.assign_config(model_object_new.config);
if (! object_diff.empty() || object_config_changed || num_extruders_changed ) {
PrintObjectConfig new_config = PrintObject::object_config_from_model_object(m_default_object_config, model_object, num_extruders );
PrintObjectConfig new_config = PrintObject::object_config_from_model_object(m_default_object_config, model_object, num_extruders, print_variant_index);
for (const PrintObjectStatus &print_object_status : print_object_status_db.get_range(model_object)) {
t_config_option_keys diff = print_object_status.print_object->config().diff(new_config);
if (! diff.empty()) {
@@ -1541,10 +1679,10 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// Generate a list of trafos and XY offsets for instances of a ModelObject
// Producing the config for PrintObject on demand, caching it at print_object_last.
const PrintObject *print_object_last = nullptr;
auto print_object_apply_config = [this, &print_object_last, model_object, num_extruders ](PrintObject *print_object) {
auto print_object_apply_config = [this, &print_object_last, model_object, num_extruders, &print_variant_index](PrintObject *print_object) {
print_object->config_apply(print_object_last ?
print_object_last->config() :
PrintObject::object_config_from_model_object(m_default_object_config, *model_object, num_extruders ));
PrintObject::object_config_from_model_object(m_default_object_config, *model_object, num_extruders, print_variant_index));
print_object_last = print_object;
};
if (old.empty()) {
@@ -1654,7 +1792,14 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
m_default_object_config.apply_only(new_full_config, new_changed_keys, true);
// Handle changes to regions config defaults
m_default_region_config.apply_only(new_full_config, new_changed_keys, true);
// Orca: keep the pre-expansion snapshot in sync with this late normalization pass.
// The engine map write-back rebuilds m_full_print_config from m_ori_full_print_config
// after slicing; a stale snapshot would resurrect the un-normalized values (e.g.
// enable_prime_tower on a single-filament print) in the dumped config and spuriously
// re-invalidate the g-code on the next apply.
m_ori_full_print_config.apply_only(new_full_config, new_changed_keys, true);
m_full_print_config = std::move(new_full_config);
update_filament_self_index_cache();
}
// All regions now have distinct settings.
@@ -1715,7 +1860,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
for (auto it = it_print_object; it != it_print_object_end; ++it)
if ((*it)->m_shared_regions != nullptr)
update_apply_status((*it)->invalidate_state_by_config_options(old_config, new_config, diff_keys));
})) {
},
print_variant_index)) {
// Regions are valid, just keep them.
} else {
// Regions were reshuffled.
@@ -1737,6 +1883,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
num_extruders ,
print_object.is_mm_painted() ? 0.f : float(print_object.config().xy_contour_compensation.value),
painting_extruders,
print_variant_index,
print_object.is_fuzzy_skin_painted());
}
for (auto it = it_print_object; it != it_print_object_end; ++it)
File diff suppressed because it is too large Load Diff
+230 -7
View File
@@ -46,6 +46,14 @@ enum GCodeFlavor : unsigned char {
gcfNoExtrusion
};
// How a filament is used across the model. Part of the multi-nozzle grouping data; not yet
// read by the shipping slicer — the nozzle-centric FilamentGroup engine consumes it.
enum FilamentUsageType {
SupportOnly,
ModelOnly,
Hybrid
};
enum class FuzzySkinType {
None,
@@ -62,6 +70,19 @@ enum class FuzzySkinMode {
Combined,
};
// ORCA: direction in which top_surface_expansion grows the top surfaces.
enum class TopSurfaceExpansionDirection {
InwardAndOutward,
Inward,
Outward,
};
enum class CenterOfSurfacePattern {
Each_Surface,
Each_Model,
Each_Assembly,
};
enum class NoiseType {
Classic,
Perlin,
@@ -97,6 +118,34 @@ enum InfillPattern : int {
ipCount,
};
// Orca: Infill patterns whose alignment origin follows the fill bounding box, so the
// "separated_infills" option can re-center them per connected body. Patterns evaluated in
// absolute/global coordinates (Gyroid, TPMS, Honeycomb, CrossHatch, ...) or that are shape-relative
// (Concentric) ignore that bounding box and are therefore excluded.
inline bool is_separable_infill_pattern(InfillPattern pattern)
{
switch (pattern) {
case ipRectilinear:
case ipAlignedRectilinear:
case ipZigZag:
case ipCrossZag:
case ipLockedZag:
case ipGrid:
case ipTriangles:
case ipStars: // tri-hexagon
case ipCubic:
case ipQuarterCubic:
case ipLateralHoneycomb:
case ipLateralLattice:
case ipHilbertCurve:
case ipArchimedeanChords:
case ipOctagramSpiral:
return true;
default:
return false;
}
}
enum class IroningType {
NoIroning,
TopSurfaces,
@@ -144,6 +193,15 @@ enum class WallDirection
Count,
};
// Orca: print order of surface fill loops/fragments for center-based fill patterns
// (Concentric, Archimedean Chords, Octagram Spiral).
enum class SurfaceFillOrder {
Default,
Outward,
Inward,
Count,
};
//BBS
enum class PrintSequence {
ByLayer,
@@ -156,6 +214,8 @@ enum class PrintOrder
{
Default,
AsObjectList,
BestOfStrategies, // run all custom strategies, pick the shortest total path
Snake, // snake-like row traversal (back-and-forth) + 2-opt
Count,
};
@@ -300,6 +360,12 @@ enum class PerimeterGeneratorType
Arachne
};
enum class ToolChangeOrderingType
{
Default,
Cyclic,
};
// BBS
enum OverhangFanThreshold {
Overhang_threshold_none = 0,
@@ -335,6 +401,13 @@ enum LayerSeq {
flsCustomize
};
enum FanDirection {
fdUndefine = 0,
fdLeft,
fdRight,
fdBoth
};
static std::unordered_map<NozzleType, std::string>NozzleTypeEumnToStr = {
{NozzleType::ntUndefine, "undefine"},
{NozzleType::ntHardenedSteel, "hardened_steel"},
@@ -418,24 +491,49 @@ enum ExtruderType {
enum NozzleVolumeType {
nvtStandard = 0,
nvtHighFlow,
nvtMaxNozzleVolumeType = nvtHighFlow
nvtHybrid, // extruder holds a mix of Standard and High Flow sub-nozzles; selectable only for extruders
// with more than one sub-nozzle (extruder_max_nozzle_count > 1); matched as Standard for
// preset lookup and never emitted in profile variant strings
nvtTPUHighFlow, // physical variant, used on H2D/H2DP 0.4 nozzles only
// Integer values are serialized as raw ints in 3mf plate metadata and device MQTT, so they MUST stay stable.
nvtMaxNozzleVolumeType = nvtTPUHighFlow
};
enum FilamentMapMode {
fmmAutoForFlush,
fmmAutoForMatch,
fmmManual,
fmmNozzleManual, // Fully-manual filament->physical-nozzle mapping (filament_nozzle_map). Kept ordered right after fmmManual so every `< fmmManual` "is-auto" check stays correct.
fmmDefault
};
// All auto modes are ordered before fmmManual (see the enum ordering note above).
inline bool is_auto_filament_map_mode(FilamentMapMode mode) {
return mode < fmmManual;
}
// Dual-extruder purge control. Default reproduces the current
// per-extruder flush_multiplier + filament_prime_volume behaviour, so absent/default is inert.
// Saving -> reduce prime volume to 15 mm3; Fast -> use flush_multiplier_fast + filament_flush_temp_fast.
enum PrimeVolumeMode {
pvmDefault = 0,
pvmSaving,
pvmFast
};
extern std::string get_extruder_variant_string(ExtruderType extruder_type, NozzleVolumeType nozzle_volume_type);
// Base slot lookup: scans a variant list (paired with its 1-based extruder/filament ids) for the
// entry matching the given extruder/volume type and id. Returns 0 when no entry matches.
extern int get_config_index_base(NozzleVolumeType volume_type, ExtruderType extruder_type, int variant_id_1based, const std::vector<std::string>& variant_list, const std::vector<int>& variant_ids_1based);
static std::set<NozzleVolumeType> get_valid_nozzle_volume_type() {
std::set<NozzleVolumeType> type;
for (int i = 0; i <= nvtMaxNozzleVolumeType; ++i) {
auto t = static_cast<NozzleVolumeType>(i);
// TODO: Orca: Support hybrid
//if (t == nvtHybrid) continue;
// Hybrid is not a physical nozzle variant: presets never define it, so it must not
// produce a variant string.
if (t == nvtHybrid) continue;
type.insert(t);
}
return type;
@@ -521,11 +619,20 @@ static std::string get_bed_temp_1st_layer_key(const BedType type)
}
extern const std::vector<std::string> filament_extruder_override_keys;
// Full override-key check incl. filament_retract_length_nc (defined outside the generator list).
extern bool is_filament_extruder_override_key(const std::string &opt_key);
// for parse extruder_ams_count
extern std::vector<std::map<int, int>> get_extruder_ams_count(const std::vector<std::string> &strs);
extern std::vector<std::string> save_extruder_ams_count_to_string(const std::vector<std::map<int, int>> &extruder_ams_count);
// maps a full extruder variant string (e.g. "Direct Drive High Flow") to its NozzleVolumeType; nvtHybrid if unparsable
extern NozzleVolumeType convert_to_nvt_type(const std::string& variant_str);
// for parse extruder_nozzle_stats (per-extruder physical nozzle inventory by volume type)
extern std::vector<std::map<NozzleVolumeType, int>> get_extruder_nozzle_stats(const std::vector<std::string> &strs);
extern std::vector<std::string> save_extruder_nozzle_stats_to_string(const std::vector<std::map<NozzleVolumeType, int>> &extruder_nozzle_stats);
#define CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NAME) \
template<> const t_config_enum_names& ConfigOptionEnum<NAME>::get_enum_names(); \
template<> const t_config_enum_values& ConfigOptionEnum<NAME>::get_enum_values();
@@ -534,6 +641,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PrinterTechnology)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeFlavor)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TopSurfaceExpansionDirection)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(WipeTowerType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern)
@@ -561,7 +669,9 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PrintHostType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AuthorizationType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(WipeTowerWallType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ToolChangeOrderingType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder)
#undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
@@ -613,6 +723,23 @@ class StaticPrintConfig;
// Minimum object distance for arrangement, based on printer technology.
double min_object_distance(const ConfigBase &cfg);
// One (extruder type x nozzle volume type) parameter variant a filament prints through, plus a
// representative physical extruder observed using it. Ordering (and set-dedup identity) covers
// the variant pair only, so the same variant reached through two extruders keeps one config slot.
struct FilamentVariantUse
{
ExtruderType extruder_type{etDirectDrive};
NozzleVolumeType nozzle_volume_type{nvtStandard};
int extruder_id{0}; // 0-based, first extruder seen using this variant
bool operator<(const FilamentVariantUse &other) const
{
if (extruder_type != other.extruder_type)
return extruder_type < other.extruder_type;
return nozzle_volume_type < other.nozzle_volume_type;
}
};
// Slic3r dynamic configuration, used to override the configuration
// per object, per modification volume or per printing material.
// The dynamic configuration is also used to store user modifications of the print global parameters,
@@ -670,9 +797,26 @@ public:
//BBS
bool is_using_different_extruders();
bool support_different_extruders(int& extruder_count) const;
// Counts the config slots of a printer: one per (extruder x nozzle volume type) as described by
// extruder_nozzle_stats, or simply one per extruder when the stats are absent/mismatched.
// Fills nozzle_volume_types with each extruder's volume types in ascending enum order.
int get_extruder_nozzle_volume_count(int extruder_count, std::vector<std::vector<NozzleVolumeType>>& nozzle_volume_types) const;
int get_index_for_extruder(int extruder_or_filament_id, std::string id_name, ExtruderType extruder_type, NozzleVolumeType nozzle_volume_type, std::string variant_name, unsigned int stride = 1) const;
void update_values_to_printer_extruders(DynamicPrintConfig& printer_config, std::set<std::string>& key_set, std::string id_name, std::string variant_name, unsigned int stride = 1, unsigned int extruder_id = 0);
void update_values_to_printer_extruders_for_multiple_filaments(DynamicPrintConfig& printer_config, std::set<std::string>& key_set, std::string id_name, std::string variant_name);
std::vector<int> update_values_to_printer_extruders(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::vector<std::vector<NozzleVolumeType>>& nv_types,
std::set<std::string>& key_set, std::string id_name, std::string variant_name, unsigned int stride = 1, unsigned int extruder_id = 0, NozzleVolumeType filament_nvt = nvtStandard);
void update_values_to_printer_extruders_for_multiple_filaments(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::set<std::string>& key_set, std::string id_name, std::string variant_name);
// Rebuilds the per-slot filament arrays from a per-layer grouping outcome: a filament that
// prints through several (extruder x nozzle volume type) variants keeps one slot per variant
// (unlike the single-slot rebuild above), so layer-aware consumers can resolve the slot the
// current layer actually prints with. Filaments absent from filament_variant_uses keep a
// single slot resolved from filament_map / filament_volume_map. When slot_machine_indices is
// non-null it receives one machine-variant slot index per output slot (the nil-value fallback
// keying for the extruder retract overrides; a per-filament map cannot index expanded arrays).
void update_filament_config_values_for_multiple_extruders(DynamicPrintConfig& printer_config,
const std::unordered_map<int, std::vector<FilamentVariantUse>>& filament_variant_uses,
int extruder_count, int extruder_nozzle_volume_count,
std::set<std::string>& key_set, std::string id_name, std::string variant_name,
std::vector<int>* slot_machine_indices = nullptr);
void update_non_diff_values_to_base_config(DynamicPrintConfig& new_config, const t_config_option_keys& keys, const std::set<std::string>& different_keys, std::string extruder_id_name, std::string extruder_variant_name,
std::set<std::string>& key_set1, std::set<std::string>& key_set2);
@@ -698,8 +842,11 @@ extern std::set<std::string> printer_options_with_variant_1;
extern std::set<std::string> printer_options_with_variant_2;
extern std::set<std::string> empty_options;
extern std::set<std::string> filament_dev_options;
extern void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPrintConfig& dest_config, std::vector<int> variant_index, std::set<std::string>& key_set1, int stride = 1);
extern void compute_filament_override_value(const std::string& opt_key, const ConfigOption *opt_old_machine, const ConfigOption *opt_new_machine, const ConfigOption *opt_new_filament, const DynamicPrintConfig& new_full_config,
t_config_option_keys& diff_keys, DynamicPrintConfig& filament_overrides, std::vector<int>& f_maps);
t_config_option_keys& diff_keys, DynamicPrintConfig& filament_overrides, std::vector<int>& f_map_indices);
void handle_legacy_sla(DynamicPrintConfig &config);
@@ -935,6 +1082,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloat, brim_width))
((ConfigOptionFloat, brim_ears_detection_length))
((ConfigOptionFloat, brim_ears_max_angle))
((ConfigOptionBool, brim_ears_outer_only))
((ConfigOptionFloat, skirt_start_angle))
((ConfigOptionBool, bridge_no_support))
((ConfigOptionFloat, elefant_foot_compensation))
@@ -1102,6 +1250,8 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionPercent, bottom_surface_density))
((ConfigOptionEnum<InfillPattern>, top_surface_pattern))
((ConfigOptionEnum<InfillPattern>, bottom_surface_pattern))
((ConfigOptionEnum<SurfaceFillOrder>, top_surface_fill_order))
((ConfigOptionEnum<SurfaceFillOrder>, bottom_surface_fill_order))
((ConfigOptionEnum<InfillPattern>, internal_solid_infill_pattern))
((ConfigOptionFloatOrPercent, outer_wall_line_width))
((ConfigOptionFloatsNullable, outer_wall_speed))
@@ -1115,6 +1265,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionString, sparse_infill_rotate_template))
((ConfigOptionPercent, sparse_infill_density))
((ConfigOptionEnum<InfillPattern>, sparse_infill_pattern))
((ConfigOptionPercent, sparse_infill_smooth_factor))
((ConfigOptionFloat, lateral_lattice_angle_1))
((ConfigOptionFloat, lateral_lattice_angle_2))
((ConfigOptionFloat, infill_overhang_angle))
@@ -1122,6 +1273,8 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloat, lightning_prune_angle))
((ConfigOptionFloat, lightning_straightening_angle))
((ConfigOptionBool, align_infill_direction_to_model))
((ConfigOptionEnum<CenterOfSurfacePattern>, center_of_surface_pattern))
((ConfigOptionBool, separated_infills))
((ConfigOptionString, extra_solid_infills))
((ConfigOptionEnum<FuzzySkinType>, fuzzy_skin))
((ConfigOptionFloat, fuzzy_skin_thickness))
@@ -1187,6 +1340,9 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloatOrPercent, top_surface_line_width))
((ConfigOptionInt, top_shell_layers))
((ConfigOptionFloat, top_shell_thickness))
((ConfigOptionFloat, top_surface_expansion))
((ConfigOptionFloat, top_surface_expansion_margin))
((ConfigOptionEnum<TopSurfaceExpansionDirection>, top_surface_expansion_direction))
((ConfigOptionFloatsNullable, top_surface_speed))
//BBS
((ConfigOptionBoolsNullable, enable_overhang_speed))
@@ -1225,6 +1381,8 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionBool, hole_to_polyhole))
((ConfigOptionFloatOrPercent, hole_to_polyhole_threshold))
((ConfigOptionBool, hole_to_polyhole_twisted))
((ConfigOptionInt, hole_to_polyhole_max_edges))
((ConfigOptionBool, overhang_reverse))
((ConfigOptionBool, overhang_reverse_internal_only))
((ConfigOptionFloatOrPercent, overhang_reverse_threshold))
@@ -1295,6 +1453,12 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloats, machine_min_travel_rate))
// M205 S... [mm/sec]
((ConfigOptionFloats, machine_min_extruding_rate))
// Bedslinger mass/force model: drive the per-layer Y-axis
// acceleration limit (curr_y_acceleration_limit) and the printed-mass check.
// Default 0 => inactive for every existing printer (mass model reads them as disabled).
((ConfigOptionFloat, machine_max_force_Y))
((ConfigOptionFloat, machine_bed_mass_Y))
((ConfigOptionFloat, machine_max_printed_mass))
//resonance avoidance ported from qidi slicer
((ConfigOptionBool, resonance_avoidance))
@@ -1349,6 +1513,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionStrings, filament_vendor))
((ConfigOptionBools, filament_is_support))
((ConfigOptionInts, filament_printable))
((ConfigOptionInts, filament_extruder_compatibility))
((ConfigOptionFloats, filament_change_length))
((ConfigOptionFloats, filament_cost))
((ConfigOptionStrings, default_filament_colour))
@@ -1357,14 +1522,20 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionInts, required_nozzle_HRC))
((ConfigOptionEnum<FilamentMapMode>, filament_map_mode))
((ConfigOptionInts, filament_map))
((ConfigOptionInts, filament_volume_map))
((ConfigOptionInts, filament_nozzle_map))
((ConfigOptionInts, filament_map_2)) //used for multi nozzle, map filament to the index identified by extruder+nozzle_volume_type
//((ConfigOptionInts, filament_extruder_id))
((ConfigOptionStrings, filament_extruder_variant))
((ConfigOptionInts, filament_self_index))
((ConfigOptionBool, support_object_skip_flush))
((ConfigOptionEnum<BedTempFormula>, bed_temperature_formula))
((ConfigOptionInts, physical_extruder_map))
((ConfigOptionIntsNullable, nozzle_flush_dataset))
((ConfigOptionFloatsNullable, filament_flush_volumetric_speed))
((ConfigOptionIntsNullable, filament_flush_temp))
// Fast-purge flush temperature; consumed only when prime_volume_mode==pvmFast.
((ConfigOptionIntsNullable, filament_flush_temp_fast))
// BBS
((ConfigOptionBool, scan_first_layer))
((ConfigOptionEnum<PowerLossRecoveryMode>, enable_power_loss_recovery))
@@ -1376,7 +1547,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionBool, gcode_add_line_number))
((ConfigOptionBool, bbl_bed_temperature_gcode))
((ConfigOptionEnum<GCodeFlavor>, gcode_flavor))
((ConfigOptionBool, gcode_skip_config_block))
((ConfigOptionFloat, time_cost))
((ConfigOptionString, layer_change_gcode))
((ConfigOptionString, time_lapse_gcode))
@@ -1388,6 +1559,9 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionPercents, retract_before_wipe))
// Orca
((ConfigOptionPercents, retract_after_wipe))
((ConfigOptionFloats, retraction_length))
((ConfigOptionFloats, retract_length_toolchange))
((ConfigOptionInt, enable_long_retraction_when_cut))
@@ -1411,6 +1585,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionBool, single_extruder_multi_material))
((ConfigOptionBool, manual_filament_change))
((ConfigOptionBool, single_extruder_multi_material_priming))
((ConfigOptionEnum<ToolChangeOrderingType>, toolchange_ordering))
((ConfigOptionBool, wipe_tower_no_sparse_layers))
((ConfigOptionString, change_filament_gcode))
((ConfigOptionString, change_extrusion_role_gcode))
@@ -1425,12 +1600,16 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionEnumsGenericNullable,nozzle_type))
((ConfigOptionInt, nozzle_hrc))
((ConfigOptionBool, auxiliary_fan))
((ConfigOptionEnum<FanDirection>, fan_direction))
((ConfigOptionBool, support_air_filtration))
((ConfigOptionBool, support_cooling_filter))
((ConfigOptionBool, cooling_filter_enabled))
((ConfigOptionEnum<PrinterStructure>,printer_structure))
((ConfigOptionBool, support_chamber_temp_control))
((ConfigOptionEnumsGeneric, extruder_type))
((ConfigOptionEnumsGeneric, nozzle_volume_type))
((ConfigOptionStrings, extruder_ams_count))
((ConfigOptionStrings, extruder_nozzle_stats))
((ConfigOptionInts, printer_extruder_id))
((ConfigOptionInt, master_extruder_id))
((ConfigOptionStrings, printer_extruder_variant))
@@ -1481,6 +1660,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionBool, purge_in_prime_tower))
((ConfigOptionBool, enable_filament_ramming))
((ConfigOptionBool, tool_change_on_wipe_tower))
((ConfigOptionBool, wait_for_temp_on_wipe_tower))
((ConfigOptionBool, support_multi_bed_types))
((ConfigOptionBool, use_3mf))
@@ -1488,6 +1668,36 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionStrings, small_area_infill_flow_compensation_model))
((ConfigOptionBool, has_scarf_joint_seam))
// Multi-nozzle + pre-heating + nozzle-change (nc) keys. Defaults are no-ops for existing
// single-nozzle printers; new slicing paths gate on extruder_max_nozzle_count > 1.
((ConfigOptionFloat, machine_hotend_change_time))
((ConfigOptionFloat, machine_prepare_compensation_time))
((ConfigOptionBool, enable_pre_heating))
((ConfigOptionFloatsNullable, hotend_cooling_rate))
((ConfigOptionFloatsNullable, hotend_heating_rate))
((ConfigOptionFloats, filament_change_length_nc))
((ConfigOptionFloatsNullable, filament_ramming_travel_time))
((ConfigOptionIntsNullable, filament_pre_cooling_temperature))
((ConfigOptionFloatsNullable, filament_ramming_volumetric_speed))
((ConfigOptionFloatsNullable, filament_ramming_travel_time_nc))
((ConfigOptionIntsNullable, filament_pre_cooling_temperature_nc))
((ConfigOptionFloatsNullable, filament_ramming_volumetric_speed_nc))
((ConfigOptionFloatsNullable, filament_retract_length_nc))
((ConfigOptionIntsNullable, extruder_max_nozzle_count))
// Printer flag: whether the printer offers the fast-purge mode selector.
// Default false; no shipping profile sets it, so the fast-purge UI stays hidden.
((ConfigOptionBool, support_fast_purge_mode))
//ams chamber
((ConfigOptionStrings, filament_dev_ams_drying_ams_limitations))
((ConfigOptionFloats, filament_dev_ams_drying_temperature))
((ConfigOptionFloats, filament_dev_ams_drying_time))
((ConfigOptionFloats, filament_dev_ams_drying_heat_distortion_temperature))
((ConfigOptionFloats, filament_dev_chamber_drying_bed_temperature))
((ConfigOptionFloats, filament_dev_chamber_drying_time))
((ConfigOptionFloats, filament_dev_drying_softening_temperature))
((ConfigOptionFloats, filament_dev_drying_cooling_temperature))
)
// This object is mapped to Perl as Slic3r::Config::Print.
@@ -1574,6 +1784,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionBool, ooze_prevention))
((ConfigOptionString, filename_format))
((ConfigOptionStrings, post_process))
((ConfigOptionStrings, slicing_pipeline_plugin))
((ConfigOptionString, print_plugin_config_overrides))
((ConfigOptionString, printer_model))
((ConfigOptionFloat, resolution))
((ConfigOptionFloats, retraction_minimum_travel))
@@ -1634,7 +1846,16 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
// BBS: wipe tower is only used for priming
((ConfigOptionFloat, prime_volume))
// Nozzle-change (nc) prime volume + pre-heat delta
((ConfigOptionFloats, filament_prime_volume))
((ConfigOptionFloats, filament_prime_volume_nc))
((ConfigOptionFloatsNullable, filament_preheat_temperature_delta))
((ConfigOptionFloats, flush_multiplier))
// Fast-purge mode. Kept out of the g-code config block (banned_keys in
// GCode::append_full_config) so registering them leaves the shipping fleet's g-code byte-identical;
// consumed only on the prime_volume_mode==pvmFast / pvmSaving branch (default pvmDefault = inert).
((ConfigOptionEnum<PrimeVolumeMode>, prime_volume_mode))
((ConfigOptionFloats, flush_multiplier_fast))
((ConfigOptionFloat, z_offset))
// BBS: project filaments
((ConfigOptionFloats, filament_colour_new))
@@ -1642,6 +1863,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionFloatsNullable, nozzle_volume))
((ConfigOptionPoints, start_end_points))
((ConfigOptionEnum<TimelapseType>, timelapse_type))
// Corexy farthest-point timelapse (default false → inert for existing printers)
((ConfigOptionBool, farthest_point_timelapse))
((ConfigOptionString, thumbnails))
// BBS: move from PrintObjectConfig
((ConfigOptionBool, independent_support_layer_height))
+196 -30
View File
@@ -4,6 +4,7 @@
#include "Print.hpp"
#include "BoundingBox.hpp"
#include "ClipperUtils.hpp"
#include "Clipper2Utils.hpp"
#include "ElephantFootCompensation.hpp"
#include "Geometry.hpp"
#include "I18N.hpp"
@@ -97,7 +98,7 @@ PrintObject::PrintObject(Print* print, ModelObject* model_object, const Transfor
// snug height and an approximate bounding box in XY.
BoundingBoxf3 bbox = model_object->raw_bounding_box();
Vec3d bbox_center = bbox.center();
// We may need to rotate the bbox / bbox_center from the original instance to the current instance.
double z_diff = Geometry::rotation_diff_z(model_object->instances.front()->get_rotation(), instances.front().model_instance->get_rotation());
if (std::abs(z_diff) > EPSILON) {
@@ -157,10 +158,10 @@ std::vector<std::reference_wrapper<const PrintRegion>> PrintObject::all_regions(
return out;
}
Polygons create_polyholes(const Point center, const coord_t radius, const coord_t nozzle_diameter, bool multiple)
Polygons create_polyholes(const Point center, const coord_t radius, const coord_t nozzle_diameter, bool multiple, int max_edges)
{
// n = max(round(2 * d), 3); // for 0.4mm nozzle
size_t nb_edges = (int)std::max(3, (int)std::round(4.0 * unscaled(radius) * 0.4 / unscaled(nozzle_diameter)));
size_t nb_edges = (int)std::min(max_edges, std::max(3, (int)std::round(4.0 * unscaled(radius) * 0.4 / unscaled(nozzle_diameter))));
// cylinder(h = h, r = d / cos (180 / n), $fn = n);
//create x polyholes by rotation if multiple
int nb_polyhole = 1;
@@ -190,8 +191,8 @@ void PrintObject::_transform_hole_to_polyholes()
{
// get all circular holes for each layer
// the id is center-diameter-extruderid
//the tuple is Point center; float diameter_max; int extruder_id; coord_t max_variation; bool twist;
std::vector<std::vector<std::pair<std::tuple<Point, float, int, coord_t, bool>, Polygon*>>> layerid2center;
//the tuple is Point center; float diameter_max; int extruder_id; coord_t max_variation; bool twist; int max_edges;
std::vector<std::vector<std::pair<std::tuple<Point, float, int, coord_t, bool, int>, Polygon*>>> layerid2center;
for (size_t i = 0; i < this->m_layers.size(); i++) layerid2center.emplace_back();
tbb::parallel_for(
tbb::blocked_range<size_t>(0, m_layers.size()),
@@ -230,9 +231,10 @@ void PrintObject::_transform_hole_to_polyholes()
// SCALED_EPSILON was a bit too harsh. Now using a config, as some may want some harsh setting and some don't.
coord_t max_variation = std::max(SCALED_EPSILON, scale_(this->m_layers[layer_idx]->m_regions[region_idx]->region().config().hole_to_polyhole_threshold.get_abs_value(unscaled(diameter_sum / hole.points.size()))));
bool twist = this->m_layers[layer_idx]->m_regions[region_idx]->region().config().hole_to_polyhole_twisted.value;
int max_edges = this->m_layers[layer_idx]->m_regions[region_idx]->region().config().hole_to_polyhole_max_edges.value;
if (diameter_max - diameter_min < max_variation * 2 && diameter_line_max - diameter_line_min < max_variation * 2) {
layerid2center[layer_idx].emplace_back(
std::tuple<Point, float, int, coord_t, bool>{center, diameter_max, layer->m_regions[region_idx]->region().config().outer_wall_filament_id.value, max_variation, twist}, & hole);
std::tuple<Point, float, int, coord_t, bool, int>{center, diameter_max, layer->m_regions[region_idx]->region().config().outer_wall_filament_id.value, max_variation, twist, max_edges}, & hole);
}
}
}
@@ -243,14 +245,14 @@ void PrintObject::_transform_hole_to_polyholes()
}
});
//sort holes per center-diameter
std::map<std::tuple<Point, float, int, coord_t, bool>, std::vector<std::pair<Polygon*, int>>> id2layerz2hole;
std::map<std::tuple<Point, float, int, coord_t, bool, int>, std::vector<std::pair<Polygon*, int>>> id2layerz2hole;
//search & find hole that span at least X layers
const size_t min_nb_layers = 2;
for (size_t layer_idx = 0; layer_idx < this->m_layers.size(); ++layer_idx) {
for (size_t hole_idx = 0; hole_idx < layerid2center[layer_idx].size(); ++hole_idx) {
//get all other same polygons
std::tuple<Point, float, int, coord_t, bool>& id = layerid2center[layer_idx][hole_idx].first;
std::tuple<Point, float, int, coord_t, bool, int>& id = layerid2center[layer_idx][hole_idx].first;
float max_z = layers()[layer_idx]->print_z;
std::vector<std::pair<Polygon*, int>> holes;
holes.emplace_back(layerid2center[layer_idx][hole_idx].second, layer_idx);
@@ -258,7 +260,7 @@ void PrintObject::_transform_hole_to_polyholes()
if (layers()[search_layer_idx]->print_z - layers()[search_layer_idx]->height - max_z > EPSILON) break;
//search an other polygon with same id
for (size_t search_hole_idx = 0; search_hole_idx < layerid2center[search_layer_idx].size(); ++search_hole_idx) {
std::tuple<Point, float, int, coord_t, bool>& search_id = layerid2center[search_layer_idx][search_hole_idx].first;
std::tuple<Point, float, int, coord_t, bool, int>& search_id = layerid2center[search_layer_idx][search_hole_idx].first;
if (std::get<2>(id) == std::get<2>(search_id)
&& std::get<0>(id).distance_to(std::get<0>(search_id)) < std::get<3>(id)
&& std::abs(std::get<1>(id) - std::get<1>(search_id)) < std::get<3>(id)
@@ -279,7 +281,7 @@ void PrintObject::_transform_hole_to_polyholes()
}
//create a polyhole per id and replace holes points by it.
for (auto entry : id2layerz2hole) {
Polygons polyholes = create_polyholes(std::get<0>(entry.first), std::get<1>(entry.first), scale_(print()->config().nozzle_diameter.get_at(std::get<2>(entry.first) - 1)), std::get<4>(entry.first));
Polygons polyholes = create_polyholes(std::get<0>(entry.first), std::get<1>(entry.first), scale_(print()->config().nozzle_diameter.get_at(std::get<2>(entry.first) - 1)), std::get<4>(entry.first), std::get<5>(entry.first));
for (auto& poly_to_replace : entry.second) {
Polygon polyhole = polyholes[poly_to_replace.second % polyholes.size()];
//search the clone in layers->slices
@@ -703,6 +705,72 @@ void PrintObject::infill()
if (this->set_started(posInfill)) {
m_print->set_status(35, L("Generating infill toolpath"));
// Orca: precompute the object's 3D connected bodies for separated infills / per-model
// centering. Two islands belong to the same body when their slices overlap on adjacent
// layers; islands that only overlap in top-down projection but never touch (e.g. interleaved
// chain links) stay separate, matching "split to objects". Each layer island then records
// the full bounding box of its body, so its infill is centered on that body as if it were
// sliced alone. Done once here, before the parallel fill, and only when a region needs it.
bool needs_separated_components = false;
for (size_t i = 0; i < this->num_printing_regions(); ++ i) {
const PrintRegionConfig &rc = this->printing_region(i).config();
if (rc.separated_infills || rc.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model) {
needs_separated_components = true;
break;
}
}
// Fast path: the feature only changes anything when the object is made of more than one
// connected body. Detect that cheaply the same way as "Split to objects" — more than one
// model part, or a single part whose mesh is splittable (is_splittable() is cached). A single
// body already shares the object center, i.e. the default, so skip the connectivity pass.
if (needs_separated_components) {
int parts = 0;
const ModelVolume *first_part = nullptr;
for (const ModelVolume *v : this->model_object()->volumes)
if (v->is_model_part()) { ++ parts; first_part = v; }
if (parts <= 1 && ! (first_part != nullptr && first_part->is_splittable()))
needs_separated_components = false;
}
for (Layer *layer : m_layers)
layer->lslices_separated_component_bboxes.clear();
if (needs_separated_components) {
const size_t nl = m_layers.size();
std::vector<size_t> offset(nl + 1, 0); // flat index of the first island of each layer
for (size_t i = 0; i < nl; ++ i)
offset[i + 1] = offset[i] + m_layers[i]->lslices.size();
const size_t nreg = offset[nl];
// Union-find over every (layer, island).
std::vector<size_t> parent(nreg);
for (size_t i = 0; i < nreg; ++ i) parent[i] = i;
auto find = [&parent](size_t x) {
while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
return x;
};
auto unite = [&](size_t a, size_t b) { a = find(a); b = find(b); if (a != b) parent[a] = b; };
// Join islands that overlap between two consecutive layers.
for (size_t i = 0; i + 1 < nl; ++ i) {
const Layer *la = m_layers[i], *lb = m_layers[i + 1];
for (size_t a = 0; a < la->lslices.size(); ++ a)
for (size_t b = 0; b < lb->lslices.size(); ++ b)
if (la->lslices_bboxes[a].overlap(lb->lslices_bboxes[b]) &&
! intersection_ex(la->lslices[a], lb->lslices[b]).empty())
unite(offset[i] + a, offset[i + 1] + b);
}
// Full bounding box of each body, indexed by its union-find root.
std::vector<BoundingBox> body_bbox(nreg);
for (size_t i = 0; i < nl; ++ i)
for (size_t a = 0; a < m_layers[i]->lslices.size(); ++ a)
body_bbox[find(offset[i] + a)].merge(m_layers[i]->lslices_bboxes[a]);
// Store the body bbox for every island.
for (size_t i = 0; i < nl; ++ i) {
Layer *layer = m_layers[i];
layer->lslices_separated_component_bboxes.resize(layer->lslices.size());
for (size_t a = 0; a < layer->lslices.size(); ++ a)
layer->lslices_separated_component_bboxes[a] = body_bbox[find(offset[i] + a)];
}
}
const auto& adaptive_fill_octree = this->m_adaptive_fill_octrees.first;
const auto& support_fill_octree = this->m_adaptive_fill_octrees.second;
@@ -1107,6 +1175,7 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "brim_type"
|| opt_key == "brim_ears_max_angle"
|| opt_key == "brim_ears_detection_length"
|| opt_key == "brim_ears_outer_only"
// BBS: brim generation depends on printing speed
|| opt_key == "outer_wall_speed"
|| opt_key == "small_perimeter_speed"
@@ -1205,6 +1274,7 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "hole_to_polyhole"
|| opt_key == "hole_to_polyhole_threshold"
|| opt_key == "hole_to_polyhole_twisted"
|| opt_key == "hole_to_polyhole_max_edges"
) {
steps.emplace_back(posSlice);
} else if (opt_key == "enable_support") {
@@ -1295,6 +1365,8 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "infill_combination_max_layer_height"
|| opt_key == "bottom_shell_thickness"
|| opt_key == "top_shell_thickness"
|| opt_key == "top_surface_expansion_margin"
|| opt_key == "top_surface_expansion_direction"
|| opt_key == "minimum_sparse_infill_area"
|| opt_key == "sparse_infill_filament_id"
|| opt_key == "internal_solid_filament_id"
@@ -1321,13 +1393,16 @@ bool PrintObject::invalidate_state_by_config_options(
} else if (
opt_key == "top_surface_pattern"
|| opt_key == "bottom_surface_pattern"
|| opt_key == "top_surface_fill_order"
|| opt_key == "bottom_surface_fill_order"
|| opt_key == "internal_solid_infill_pattern"
|| opt_key == "external_fill_link_max_length"
|| opt_key == "infill_anchor"
|| opt_key == "infill_anchor_max"
|| opt_key == "top_surface_line_width"
|| opt_key == "top_surface_density"
|| opt_key == "bottom_surface_density"
|| opt_key == "center_of_surface_pattern"
|| opt_key == "separated_infills"
|| opt_key == "initial_layer_line_width"
|| opt_key == "small_area_infill_flow_compensation"
|| opt_key == "lateral_lattice_angle_1"
@@ -1335,6 +1410,7 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "infill_overhang_angle") {
steps.emplace_back(posInfill);
} else if (opt_key == "sparse_infill_pattern"
|| opt_key == "sparse_infill_smooth_factor"
|| opt_key == "symmetric_infill_y_axis"
|| opt_key == "infill_shift_step"
|| opt_key == "sparse_infill_rotate_template"
@@ -1358,6 +1434,24 @@ bool PrintObject::invalidate_state_by_config_options(
is_approx(new_density->value, 0.) || is_approx(new_density->value, 100.))
steps.emplace_back(posPerimeters);
steps.emplace_back(posPrepareInfill);
} else if (opt_key == "top_surface_density") {
// ORCA: 0% means no top solid fill, which switches off both the top surface expansion and the wall
// removal over top surfaces. Only crossing zero matters; posPerimeters cascades to posPrepareInfill.
const auto *old_density = old_config.option<ConfigOptionPercent>(opt_key);
const auto *new_density = new_config.option<ConfigOptionPercent>(opt_key);
assert(old_density && new_density);
if (is_approx(old_density->value, 0.) || is_approx(new_density->value, 0.))
steps.emplace_back(posPerimeters);
steps.emplace_back(posInfill);
} else if (opt_key == "top_surface_expansion") {
// ORCA: without the expansion the top fill never reaches the space freed by only_one_wall_top, so the
// walls over top surfaces are kept. Only crossing zero matters; posPerimeters cascades to posPrepareInfill.
const auto *old_expansion = old_config.option<ConfigOptionFloat>(opt_key);
const auto *new_expansion = new_config.option<ConfigOptionFloat>(opt_key);
assert(old_expansion && new_expansion);
if (old_expansion->value <= 0. || new_expansion->value <= 0.)
steps.emplace_back(posPerimeters);
steps.emplace_back(posPrepareInfill);
} else if (opt_key == "internal_solid_infill_line_width") {
// This value is used for calculating perimeter - infill overlap, thus perimeters need to be recalculated.
steps.emplace_back(posPerimeters);
@@ -1684,6 +1778,68 @@ void PrintObject::detect_surfaces_type()
}
}
// ORCA: Grow the top surfaces by top_surface_expansion, so the top solid infill also covers the
// material left by features rising from the middle of a top surface (filling the holes and
// joining the tops, so the features rest on solid infill). Each connected island is grown and
// clipped separately: growing one island's top across a gap into another - which may have no top
// surface at all, leaving a partially filled layer - is never allowed. The original top is
// unioned back in and bottom surfaces are never claimed, so this can only add area.
const PrintRegionConfig &region_config = layerm->region().config();
const double top_expansion = region_config.top_surface_expansion.value;
// Nothing to expand without a top fill: a 0% top surface density leaves the top layer with
// walls only, and zero top shell layers retypes it as internal in prepare_fill_surfaces().
if (top_expansion > 0. && region_config.top_shell_layers.value > 0 &&
region_config.top_surface_density.value > 0. && ! top.empty()) {
const double d = scale_(top_expansion);
const ExPolygons T = union_ex(to_expolygons(top));
// Walls are laid out on spacing, not width; and only_one_wall_top leaves a single wall over
// a top surface, which is exactly the situation handled here.
const int wall_loops = region_config.only_one_wall_top.value ? std::min(region_config.wall_loops.value, 1)
: region_config.wall_loops.value;
const double wall_band = wall_loops <= 0 ? 0. :
double(layerm->flow(frExternalPerimeter).scaled_width()) +
double(layerm->flow(frPerimeter).scaled_spacing()) * double(wall_loops - 1);
const double margin = scale_(region_config.top_surface_expansion_margin.value);
// minimum real top to act on: ignore anything thinner than ~2 top-infill lines
const float min_top = float(layerm->flow(frTopSolidInfill).scaled_width());
const auto direction = region_config.top_surface_expansion_direction.value;
ExPolygons grown;
for (const ExPolygon &island : union_ex(layerm_slices_surfaces)) {
// The top infill only exists inside the perimeters, so seed and measure from the infill
// region (the island minus the wall band), not the raw slice: a section whose exposed top
// is just the walls themselves is then skipped instead of being flooded inward. Clip the
// layer's tops to the island first, to keep the boolean ops proportional to the island.
const ExPolygons infill_region = wall_band > 0. ? offset_ex(island, -float(wall_band)) : ExPolygons{ island };
const ExPolygons island_top = intersection_ex(
ClipperUtils::clip_clipper_polygons_with_subject_bbox(T, get_extents(island).inflated(SCALED_EPSILON)),
infill_region);
if (opening_ex(island_top, min_top).empty())
continue; // no real top infill in this section - never expand into it
// Grow, then keep only what the configured direction allows, using the top's own filled
// outline (same outer edge, holes closed) to tell the two apart.
ExPolygons expanded = offset_ex_2(island_top, d, Clipper2Lib::JoinType::Miter);
if (direction != TopSurfaceExpansionDirection::InwardAndOutward) {
ExPolygons outline;
outline.reserve(island_top.size());
for (const ExPolygon &ex : island_top)
outline.emplace_back(ex.contour);
outline = union_ex(outline);
expanded = direction == TopSurfaceExpansionDirection::Inward ?
intersection_ex(expanded, outline) : // only growth into the holes
diff_ex(expanded, diff_ex(outline, island_top)); // only growth past the outer edge
}
// hold the expansion clear of the walls by the configured margin
const ExPolygons allowed = margin > 0. ? offset_ex(infill_region, -float(margin)) : infill_region;
append(grown, intersection_ex(expanded, allowed));
}
ExPolygons new_top = diff_ex(union_ex(T, grown), to_expolygons(bottom));
top.clear();
surfaces_append(top, std::move(new_top), stTop);
}
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{
static int iRun = 0;
@@ -2180,7 +2336,7 @@ void PrintObject::discover_vertical_shells()
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
Flow solid_infill_flow = layerm->flow(frSolidInfill);
coord_t infill_line_spacing = solid_infill_flow.scaled_spacing();
coord_t infill_line_spacing = solid_infill_flow.scaled_spacing();
// Find a union of perimeters below / above this surface to guarantee a minimum shell thickness.
Polygons shell;
Polygons holes;
@@ -2222,7 +2378,7 @@ void PrintObject::discover_vertical_shells()
shell = std::move(shells2);
else if (! shells2.empty()) {
polygons_append(shell, shells2);
// Running the union_ using the Clipper library piece by piece is cheaper
// Running the union_ using the Clipper library piece by piece is cheaper
// than running the union_ all at once.
shell = union_(shell);
}
@@ -2289,12 +2445,12 @@ void PrintObject::discover_vertical_shells()
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell));
svg.draw(shell);
svg.draw_outline(shell, "black", scale_(0.05));
svg.Close();
svg.Close();
}
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
#if 0
// shell = union_(shell, true);
shell = union_(shell, false);
shell = union_(shell, false);
#endif
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
shell_ex = union_safety_offset_ex(shell);
@@ -2598,7 +2754,7 @@ void PrintObject::bridge_over_infill()
}
}
// LIGHTNING INFILL SECTION - If lightning infill is used somewhere, we check the areas that are going to be bridges, and those that rely on the
// LIGHTNING INFILL SECTION - If lightning infill is used somewhere, we check the areas that are going to be bridges, and those that rely on the
// lightning infill under them get expanded. This somewhat helps to ensure that most of the extrusions are anchored to the lightning infill at the ends.
// It requires modifying this instance of print object in a specific way, so that we do not invalidate the pointers in our surfaces_by_layer structure.
if (has_lightning_infill) {
@@ -3573,13 +3729,13 @@ static void clamp_feature_filament_to_valid(ConfigOptionInt &opt, size_t num_ext
opt.value = 1;
}
PrintObjectConfig PrintObject::object_config_from_model_object(const PrintObjectConfig &default_object_config, const ModelObject &object, size_t num_extruders)
PrintObjectConfig PrintObject::object_config_from_model_object(const PrintObjectConfig &default_object_config, const ModelObject &object, size_t num_extruders, std::vector<int>& variant_index)
{
PrintObjectConfig config = default_object_config;
{
DynamicPrintConfig src_normalized(object.config.get());
src_normalized.normalize_fdm();
config.apply(src_normalized, true);
update_static_print_config_from_dynamic(config, src_normalized, variant_index, print_options_with_variant, 1);
}
// Clamp invalid extruders to the default extruder (with index 1).
clamp_exturder_to_default(config.support_filament, num_extruders);
@@ -3607,7 +3763,7 @@ struct FeatureFilamentOverrideMask
bool inner_wall_filament_id = false;
};
static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPrintConfig &in, FeatureFilamentOverrideMask &feature_overrides)
static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPrintConfig &in, FeatureFilamentOverrideMask &feature_overrides, std::vector<int>& variant_index)
{
// 1) Explicit feature filament values take precedence over base extruder fallback.
auto *opt_extruder = in.opt<ConfigOptionInt>(key_extruder);
@@ -3648,8 +3804,18 @@ static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPr
else if (it->first == "inner_wall_filament_id")
feature_overrides.inner_wall_filament_id = false;
}
} else
my_opt->set(it->second.get());
} else {
if (*my_opt != *(it->second)) {
if (my_opt->is_scalar() || variant_index.empty() || (print_options_with_variant.find(it->first) == print_options_with_variant.end()))
my_opt->set(it->second.get());
//my_opt->set(it->second.get());
else {
ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(my_opt);
const ConfigOptionVectorBase* opt_vec_dest = static_cast<const ConfigOptionVectorBase*>(it->second.get());
opt_vec_src->set_to_index(opt_vec_dest, variant_index, 1);
}
}
}
}
// 3) Apply base extruder only to features that were not explicitly overridden.
@@ -3669,7 +3835,7 @@ static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPr
}
}
PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &default_or_parent_region_config, const DynamicPrintConfig *layer_range_config, const ModelVolume &volume, size_t num_extruders)
PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &default_or_parent_region_config, const DynamicPrintConfig *layer_range_config, const ModelVolume &volume, size_t num_extruders, std::vector<int>& variant_index)
{
PrintRegionConfig config = default_or_parent_region_config;
FeatureFilamentOverrideMask feature_overrides;
@@ -3687,17 +3853,17 @@ PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &defau
if (volume.is_model_part()) {
// default_or_parent_region_config contains the Print's PrintRegionConfig.
// Override with ModelObject's PrintRegionConfig values.
apply_to_print_region_config(config, volume.get_object()->config.get(), feature_overrides);
apply_to_print_region_config(config, volume.get_object()->config.get(), feature_overrides, variant_index);
} else {
// default_or_parent_region_config contains parent PrintRegion config, which already contains ModelVolume's config.
}
apply_to_print_region_config(config, volume.config.get(), feature_overrides);
apply_to_print_region_config(config, volume.config.get(), feature_overrides, variant_index);
if (! volume.material_id().empty())
apply_to_print_region_config(config, volume.material()->config.get(), feature_overrides);
apply_to_print_region_config(config, volume.material()->config.get(), feature_overrides, variant_index);
if (layer_range_config != nullptr) {
// Not applicable to modifiers.
assert(volume.is_model_part());
apply_to_print_region_config(config, *layer_range_config, feature_overrides);
apply_to_print_region_config(config, *layer_range_config, feature_overrides, variant_index);
}
// Resolve feature defaults and clamp invalid extruders to index 1.
clamp_feature_filament_to_valid(config.sparse_infill_filament_id, num_extruders);
@@ -3747,7 +3913,7 @@ void PrintObject::update_slicing_parameters()
}
// Orca: XYZ shrinkage compensation has introduced the const Vec3d &object_shrinkage_compensation parameter to the function below
SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full_config, const ModelObject &model_object, float object_max_z, const Vec3d &object_shrinkage_compensation)
SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full_config, const ModelObject &model_object, float object_max_z, const Vec3d &object_shrinkage_compensation, std::vector<int> variant_index)
{
PrintConfig print_config;
PrintObjectConfig object_config;
@@ -3757,14 +3923,14 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full
default_region_config.apply(full_config, true);
// BBS
size_t filament_extruders = print_config.filament_diameter.size();
object_config = object_config_from_model_object(object_config, model_object, filament_extruders);
object_config = object_config_from_model_object(object_config, model_object, filament_extruders, variant_index);
std::vector<unsigned int> object_extruders;
for (const ModelVolume* model_volume : model_object.volumes)
if (model_volume->is_model_part()) {
PrintRegion::collect_object_printing_extruders(
print_config,
region_config_from_model_volume(default_region_config, nullptr, *model_volume, filament_extruders),
region_config_from_model_volume(default_region_config, nullptr, *model_volume, filament_extruders, variant_index),
object_config.brim_type != btNoBrim && object_config.brim_width > 0.,
object_extruders);
for (const std::pair<const t_layer_height_range, ModelConfig> &range_and_config : model_object.layer_config_ranges)
@@ -3776,7 +3942,7 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full
range_and_config.second.has("bottom_surface_filament_id"))
PrintRegion::collect_object_printing_extruders(
print_config,
region_config_from_model_volume(default_region_config, &range_and_config.second.get(), *model_volume, filament_extruders),
region_config_from_model_volume(default_region_config, &range_and_config.second.get(), *model_volume, filament_extruders, variant_index),
object_config.brim_type != btNoBrim && object_config.brim_width > 0.,
object_extruders);
}
+4 -4
View File
@@ -56,12 +56,12 @@ public:
int & i,
Eigen::Matrix<double, 1, 3> &closest)
{
size_t idx_unsigned = 0;
Vec3d closest_vec3d(closest);
double dist =
size_t idx_unsigned { 0 };
Vec3d closest_vec3d { Vec3d::Zero() };
const double dist {
AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
its.vertices, its.indices, m_tree, point, idx_unsigned,
closest_vec3d);
closest_vec3d) };
i = int(idx_unsigned);
closest = closest_vec3d;
return dist;
+25 -6
View File
@@ -10,6 +10,7 @@
#include "KDTreeIndirect.hpp"
#include "MutablePriorityQueue.hpp"
#include "Print.hpp"
#include "GCode/OrderingStrategies.hpp"
#include <cmath>
#include <cassert>
@@ -1103,7 +1104,7 @@ std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy) {
return chain_points(points);
}
std::vector<size_t> chain_points(const Points &points, Point *start_near)
std::vector<size_t> chain_points(const Points &points, const Point *start_near)
{
auto segment_end_point = [&points](size_t idx, bool /* first_point */) -> const Point& { return points[idx]; };
std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy<Point, decltype(segment_end_point)>(segment_end_point, points.size(), start_near);
@@ -1111,9 +1112,26 @@ std::vector<size_t> chain_points(const Points &points, Point *start_near)
out.reserve(ordered.size());
for (auto &segment_and_reversal : ordered)
out.emplace_back(segment_and_reversal.first);
return out;
}
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near)
{
std::vector<size_t> path = chain_points(points, start_near);
// Alternate 2-opt and crossing removal until convergence.
// 2-opt can create new crossings, and crossing removal can create new
// opportunities for 2-opt improvement. Break early if neither improves.
for (int iter = 0; iter < 3; ++iter) {
bool improved = tsp_2opt_improve(path, points);
improved |= tsp_remove_crossings(path, points);
if (!improved) break;
}
if (start_near == nullptr)
tsp_rotate_minimize_closing(path, points);
return path;
}
#ifndef NDEBUG
// #define DEBUG_SVG_OUTPUT
#endif /* NDEBUG */
@@ -2025,12 +2043,13 @@ std::vector<const PrintInstance*> chain_print_object_instances(const std::vector
instances.emplace_back(i, j);
}
}
auto segment_end_point = [&object_reference_points](size_t idx, bool /* first_point */) -> const Point& { return object_reference_points[idx]; };
std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy<Point, decltype(segment_end_point)>(segment_end_point, instances.size(), start_near);
// Order objects using nearest neighbor + post-processing (crossing removal + 2-opt).
std::vector<size_t> path = chain_points_with_postprocessing(object_reference_points, start_near);
std::vector<const PrintInstance*> out;
out.reserve(instances.size());
for (auto& segment_and_reversal : ordered) {
const std::pair<size_t, size_t>& inst = instances[segment_and_reversal.first];
out.reserve(path.size());
for (size_t idx : path) {
const std::pair<size_t, size_t>& inst = instances[idx];
out.emplace_back(&print_objects[inst.first]->instances()[inst.second]);
}
return out;
+3 -1
View File
@@ -15,7 +15,9 @@ namespace Slic3r {
using PolyNodes = std::vector<PolyNode*, PointsAllocator<PolyNode*>>;
}
std::vector<size_t> chain_points(const Points &points, Point *start_near = nullptr);
std::vector<size_t> chain_points(const Points &points, const Point *start_near = nullptr);
// Variant with post-processing (crossing removal + 2-opt) for object ordering.
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near = nullptr);
std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy);
std::vector<std::pair<size_t, bool>> chain_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
+56 -31
View File
@@ -65,6 +65,15 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
const bool smooth_supports = support_params.support_style != smsGrid;
SupportGeneratorLayersPtr &interface_layers = base_and_interface_layers.first;
SupportGeneratorLayersPtr &base_interface_layers = base_and_interface_layers.second;
// The user-facing interface layer counts include the contact layer. Internally,
// contact layers are generated separately, so only the remaining layers are
// projected into intermediate interface/base-interface layers here.
const size_t num_top_interface_layers = support_params.has_top_contacts ? support_params.num_top_interface_layers - 1 : 0;
const size_t num_bottom_interface_layers = support_params.has_bottom_contacts ? support_params.num_bottom_interface_layers - 1 : 0;
const size_t num_top_base_interface_layers = std::min(support_params.num_top_base_interface_layers, num_top_interface_layers);
const size_t num_bottom_base_interface_layers = std::min(support_params.num_bottom_base_interface_layers, num_bottom_interface_layers);
const size_t num_top_interface_layers_only = num_top_interface_layers - num_top_base_interface_layers;
const size_t num_bottom_interface_layers_only = num_bottom_interface_layers - num_bottom_base_interface_layers;
interface_layers.assign(intermediate_layers.size(), nullptr);
if (support_params.has_base_interfaces())
@@ -124,6 +133,8 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
};
tbb::parallel_for(tbb::blocked_range<int>(0, int(intermediate_layers.size())),
[&bottom_contacts, &top_contacts, &top_interface_layers, &top_base_interface_layers, &intermediate_layers, &insert_layer, &support_params,
num_top_interface_layers, num_bottom_interface_layers, num_top_base_interface_layers, num_bottom_base_interface_layers,
num_top_interface_layers_only, num_bottom_interface_layers_only,
snug_supports, &interface_layers, &base_interface_layers](const tbb::blocked_range<int>& range) {
// Gather the top / bottom contact layers intersecting with num_interface_layers resp. num_interface_layers_only intermediate layers above / below
// this intermediate layer.
@@ -142,16 +153,16 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
Polygons polygons_top_contact_projected_base;
Polygons polygons_bottom_contact_projected_interface;
Polygons polygons_bottom_contact_projected_base;
if (support_params.num_top_interface_layers > 0) {
if (num_top_interface_layers > 0) {
// Top Z coordinate of a slab, over which we are collecting the top / bottom contact surfaces
coordf_t top_z = intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(support_params.num_top_interface_layers) - 1)]->print_z;
coordf_t top_inteface_z = std::numeric_limits<coordf_t>::max();
if (support_params.num_top_base_interface_layers > 0)
coordf_t top_z = intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(num_top_interface_layers) - 1)]->print_z;
coordf_t top_interface_z = std::numeric_limits<coordf_t>::max();
if (num_top_base_interface_layers > 0)
// Some top base interface layers will be generated.
top_inteface_z = support_params.num_top_interface_layers_only() == 0 ?
top_interface_z = num_top_interface_layers_only == 0 ?
// Only base interface layers to generate.
- std::numeric_limits<coordf_t>::max() :
intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(support_params.num_top_interface_layers_only()) - 1)]->print_z;
intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(num_top_interface_layers_only) - 1)]->print_z;
// Move idx_top_contact_first up until above the current print_z.
idx_top_contact_first = idx_higher_or_equal(top_contacts, idx_top_contact_first, [&intermediate_layer](const SupportGeneratorLayer *layer){ return layer->print_z >= intermediate_layer.print_z; }); // - EPSILON
// Collect the top contact areas above this intermediate layer, below top_z.
@@ -160,22 +171,22 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
//FIXME maybe this adds one interface layer in excess?
if (top_contact_layer.bottom_z - EPSILON > top_z)
break;
polygons_append(top_contact_layer.bottom_z - EPSILON > top_inteface_z ? polygons_top_contact_projected_base : polygons_top_contact_projected_interface,
polygons_append(top_contact_layer.bottom_z - EPSILON > top_interface_z ? polygons_top_contact_projected_base : polygons_top_contact_projected_interface,
// For snug supports, project the overhang polygons covering the whole overhang, so that they will merge without a gap with support polygons of the other layers.
// For grid supports, merging of support regions will be performed by the projection into grid.
snug_supports ? *top_contact_layer.overhang_polygons : top_contact_layer.polygons);
}
}
if (support_params.num_bottom_interface_layers > 0) {
if (num_bottom_interface_layers > 0) {
// Bottom Z coordinate of a slab, over which we are collecting the top / bottom contact surfaces
coordf_t bottom_z = intermediate_layers[std::max(0, idx_intermediate_layer - int(support_params.num_bottom_interface_layers) + 1)]->bottom_z;
coordf_t bottom_z = intermediate_layers[std::max(0, idx_intermediate_layer - int(num_bottom_interface_layers) + 1)]->bottom_z;
coordf_t bottom_interface_z = - std::numeric_limits<coordf_t>::max();
if (support_params.num_bottom_base_interface_layers > 0)
if (num_bottom_base_interface_layers > 0)
// Some bottom base interface layers will be generated.
bottom_interface_z = support_params.num_bottom_interface_layers_only() == 0 ?
bottom_interface_z = num_bottom_interface_layers_only == 0 ?
// Only base interface layers to generate.
std::numeric_limits<coordf_t>::max() :
intermediate_layers[std::max(0, idx_intermediate_layer - int(support_params.num_bottom_interface_layers_only()))]->bottom_z;
intermediate_layers[std::max(0, idx_intermediate_layer - int(num_bottom_interface_layers_only))]->bottom_z;
// Move idx_bottom_contact_first up until touching bottom_z.
idx_bottom_contact_first = idx_higher_or_equal(bottom_contacts, idx_bottom_contact_first, [bottom_z](const SupportGeneratorLayer *layer){ return layer->print_z >= bottom_z - EPSILON; });
// Collect the top contact areas above this intermediate layer, below top_z.
@@ -376,10 +387,7 @@ SupportGeneratorLayersPtr generate_raft_base(
Polygons trimming;
// BBS: if first layer of support is intersected with object island, it must have the same function as brim unless in nobrim mode.
// brim_object_gap is changed to 0 by default, it's no longer appropriate to use it to determine the gap of first layer support.
//if (object.has_brim())
// trimming = offset(object.layers().front()->lslices, (float)scale_(object.config().brim_object_gap.value), SUPPORT_SURFACES_OFFSET_PARAMETERS);
//else
trimming = offset(object.layers().front()->lslices, (float)scale_(support_params.gap_xy_first_layer), SUPPORT_SURFACES_OFFSET_PARAMETERS);
trimming = offset(object.layers().front()->lslices, (float) scale_(support_params.gap_xy_first_layer), SUPPORT_SURFACES_OFFSET_PARAMETERS);
if (inflate_factor_1st_layer > SCALED_EPSILON) {
// Inflate in multiple steps to avoid leaking of the support 1st layer through object walls.
auto nsteps = std::max(5, int(ceil(inflate_factor_1st_layer / support_params.first_layer_flow.scaled_width())));
@@ -1566,13 +1574,17 @@ void generate_support_toolpaths(
// Pointer to the 1st layer interface filler.
auto filler_first_layer = filler_first_layer_ptr ? filler_first_layer_ptr.get() : filler_interface.get();
// Filler for the 1st layer interface, if different from filler_interface.
auto filler_raft_contact_ptr = std::unique_ptr<Fill>(range.begin() == n_raft_layers && config.support_interface_top_layers.value == 0 ?
const bool top_interfaces_enabled = support_params.num_top_interface_layers > 0;
const bool bottom_interfaces_enabled = support_params.num_bottom_interface_layers > 0;
const coordf_t base_interface_density = top_interfaces_enabled || !bottom_interfaces_enabled ?
support_params.top_interface_density : support_params.bottom_interface_density;
auto filler_raft_contact_ptr = std::unique_ptr<Fill>(range.begin() == n_raft_layers && !top_interfaces_enabled ?
Fill::new_from_type(support_params.raft_interface_fill_pattern) : nullptr);
// Pointer to the 1st layer interface filler.
auto filler_raft_contact = filler_raft_contact_ptr ? filler_raft_contact_ptr.get() : filler_interface.get();
// Filler for the base interface (to be used for soluble interface / non soluble base, to produce non soluble interface layer below soluble interface layer).
auto filler_base_interface = std::unique_ptr<Fill>(base_interface_layers.empty() ? nullptr :
Fill::new_from_type(support_params.top_interface_density > 0.95 || support_params.with_sheath ? ipRectilinear : ipSupportBase));
Fill::new_from_type(base_interface_density > 0.95 || support_params.with_sheath ? ipRectilinear : ipSupportBase));
auto filler_support = std::unique_ptr<Fill>(Fill::new_from_type(support_params.base_fill_pattern));
filler_interface->set_bounding_box(bbox_object);
if (filler_first_layer_ptr)
@@ -1586,10 +1598,7 @@ void generate_support_toolpaths(
{
SupportLayer &support_layer = *support_layers[support_layer_id];
LayerCache &layer_cache = layer_caches[support_layer_id];
const float support_interface_angle = (config.support_interface_pattern == smipRectilinearInterlaced) ?
support_params.raft_interface_angle(support_layer.interface_id()) :
((support_params.support_style == smsGrid || config.support_interface_pattern == smipRectilinear) ?
support_params.interface_angle : support_params.raft_interface_angle(support_layer.interface_id()));
const float support_interface_angle = support_params.support_interface_angle(support_layer.interface_id());
// Find polygons with the same print_z.
SupportGeneratorLayerExtruded &bottom_contact_layer = layer_cache.bottom_contact_layer;
@@ -1622,7 +1631,9 @@ void generate_support_toolpaths(
bool raft_layer = slicing_params.interface_raft_layers && top_contact_layer.layer && is_approx(top_contact_layer.layer->print_z, slicing_params.raft_contact_top_z);
// ORCA: Organic tree uses projected contacts to build the interface stack; avoid extra bottom-contact extrusion.
const bool organic_tree = support_params.support_style == SupportMaterialStyle::smsTreeOrganic;
if (config.support_interface_top_layers == 0) {
const bool top_interfaces = support_params.num_top_interface_layers > 0;
const bool bottom_interfaces = support_params.num_bottom_interface_layers > 0;
if (!top_interfaces) {
// If no top interface layers were requested, we treat the contact layer exactly as a generic base layer.
// Don't merge the raft contact layer though.
if (support_params.can_merge_support_regions && ! raft_layer) {
@@ -1645,15 +1656,29 @@ void generate_support_toolpaths(
if (top_contact_layer.could_merge(interface_layer) && ! raft_layer)
top_contact_layer.merge(std::move(interface_layer));
}
if ((config.support_interface_top_layers == 0 || config.support_interface_bottom_layers == 0) && support_params.can_merge_support_regions) {
if (!bottom_interfaces && support_params.can_merge_support_regions) {
if (base_layer.could_merge(bottom_contact_layer))
base_layer.merge(std::move(bottom_contact_layer));
else if (base_layer.empty() && ! bottom_contact_layer.empty() && ! bottom_contact_layer.layer->bridging)
base_layer = std::move(bottom_contact_layer);
} else if (bottom_contact_layer.could_merge(top_contact_layer) && ! raft_layer) {
top_contact_layer.merge(std::move(bottom_contact_layer));
if (top_interfaces && bottom_interfaces) {
top_contact_layer.merge(std::move(bottom_contact_layer));
} else if (bottom_interfaces) {
top_contact_layer.set_polygons_to_extrude(
diff(top_contact_layer.polygons_to_extrude(), bottom_contact_layer.polygons_to_extrude()));
} else {
bottom_contact_layer.set_polygons_to_extrude(
diff(bottom_contact_layer.polygons_to_extrude(), top_contact_layer.polygons_to_extrude()));
}
} else if (bottom_contact_layer.could_merge(interface_layer) && ! organic_tree) {
bottom_contact_layer.merge(std::move(interface_layer));
const bool interface_layer_is_bottom = interface_layer.layer->layer_type == SupporLayerType::BottomInterface;
if (bottom_interfaces && interface_layer_is_bottom) {
bottom_contact_layer.merge(std::move(interface_layer));
} else {
bottom_contact_layer.set_polygons_to_extrude(
diff(bottom_contact_layer.polygons_to_extrude(), interface_layer.polygons_to_extrude()));
}
}
// Orca: For organic trees the support-material regions are generated from
@@ -1733,12 +1758,12 @@ void generate_support_toolpaths(
interface_as_base ? ExtrusionRole::erSupportMaterial : ExtrusionRole::erSupportMaterialInterface, interface_flow);
}
};
const bool top_interfaces = support_params.num_top_interface_layers > 0;
const bool bottom_interfaces = top_interfaces && support_params.num_bottom_interface_layers > 0;
extrude_interface(top_contact_layer, raft_layer ? InterfaceLayerType::RaftContact : top_interfaces ? InterfaceLayerType::TopContact : InterfaceLayerType::InterfaceAsBase);
if (!organic_tree)
extrude_interface(bottom_contact_layer, bottom_interfaces ? InterfaceLayerType::BottomContact : InterfaceLayerType::InterfaceAsBase);
extrude_interface(interface_layer, top_interfaces ? InterfaceLayerType::Interface : InterfaceLayerType::InterfaceAsBase);
const bool interface_layer_enabled = !interface_layer.empty() &&
(interface_layer.layer->layer_type == SupporLayerType::BottomInterface ? bottom_interfaces : top_interfaces);
extrude_interface(interface_layer, interface_layer_enabled ? InterfaceLayerType::Interface : InterfaceLayerType::InterfaceAsBase);
// Base interface layers under soluble interfaces
if ( ! base_interface_layer.empty() && ! base_interface_layer.polygons_to_extrude().empty()) {
Fill *filler = filler_base_interface.get();
@@ -1748,7 +1773,7 @@ void generate_support_toolpaths(
Flow interface_flow = support_params.support_material_flow.with_height(float(base_interface_layer.layer->height));
filler->angle = support_interface_angle;
filler->spacing = support_params.support_material_interface_flow.spacing();
filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / support_params.top_interface_density));
filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / base_interface_density));
fill_expolygons_generate_paths(
// Destination
base_interface_layer.extrusions,
@@ -1756,7 +1781,7 @@ void generate_support_toolpaths(
// Regions to fill
union_safety_offset_ex(base_interface_layer.polygons_to_extrude()),
// Filler and its parameters
filler, float(support_params.top_interface_density),
filler, float(base_interface_density),
// Extrusion parameters
ExtrusionRole::erSupportMaterial, interface_flow);
}
+45 -15
View File
@@ -34,7 +34,7 @@ struct SupportParameters {
{
this->num_top_interface_layers = std::max(0, object_config.support_interface_top_layers.value);
this->num_bottom_interface_layers = number_of_support_interface_bottom_layers(object_config);
this->num_bottom_interface_layers = std::max(0, number_of_support_interface_bottom_layers(object_config));
this->has_top_contacts = num_top_interface_layers > 0;
this->has_bottom_contacts = num_bottom_interface_layers > 0;
// BBS: if support interface and support base do not use the same filament, add a base layer to improve their adhesion
@@ -46,15 +46,15 @@ struct SupportParameters {
if (non_soluble_base_top) { // ORCA: Try to support soluble dense interfaces with non-soluble dense interfaces.
this->num_top_base_interface_layers = size_t(std::min(int(num_top_interface_layers) / 2, 2));
} else {
this->num_top_base_interface_layers =
(different_support_interface_filament && this->zero_gap_interface_top) ? 1 : 0;
// Keep at least one configured layer on the interface filament.
this->num_top_base_interface_layers = different_support_interface_filament && num_top_interface_layers > 1 ? 1 : 0;
}
if (non_soluble_base_bottom) { // ORCA: Try to support soluble dense interfaces with non-soluble dense interfaces.
this->num_bottom_base_interface_layers = size_t(std::min(int(num_bottom_interface_layers) / 2, 2));
} else {
this->num_bottom_base_interface_layers =
(different_support_interface_filament && this->zero_gap_interface_bottom) ? 1 : 0;
// Keep at least one configured layer on the interface filament.
this->num_bottom_base_interface_layers = different_support_interface_filament && num_bottom_interface_layers > 1 ? 1 : 0;
}
}
this->first_layer_flow = Slic3r::support_material_1st_layer_flow(&object, float(slicing_params.first_print_layer_height));
@@ -74,7 +74,7 @@ struct SupportParameters {
for (auto layer : object.layers())
this->support_layer_height_min = std::min(this->support_layer_height_min, std::max(0.01, layer->height));
if (object_config.support_interface_top_layers.value == 0) {
if (this->num_top_interface_layers == 0 && this->num_bottom_interface_layers == 0) {
// No interface layers allowed, print everything with the base support pattern.
this->support_material_interface_flow = this->support_material_flow;
}
@@ -120,8 +120,8 @@ struct SupportParameters {
this->raft_interface_density = std::min(1., this->raft_interface_flow.spacing() / raft_interface_spacing);
this->support_spacing = object_config.support_base_pattern_spacing.value + this->support_material_flow.spacing();
this->support_density = std::min(1., this->support_material_flow.spacing() / this->support_spacing);
if (object_config.support_interface_top_layers.value == 0) {
// No interface layers allowed, print everything with the base support pattern.
if (this->num_top_interface_layers == 0) {
// No top interface layers allowed; keep unused top interface parameters aligned with base support.
this->top_interface_spacing = this->support_spacing;
this->top_interface_density = this->support_density;
}
@@ -133,16 +133,20 @@ struct SupportParameters {
this->support_density > 0.95 || this->with_sheath ? ipRectilinear : ipSupportBase;
this->interface_fill_pattern = (this->top_interface_density > 0.95 ? ipRectilinear : ipSupportBase);
this->raft_interface_fill_pattern = this->raft_interface_density > 0.95 ? ipRectilinear : ipSupportBase;
const coordf_t contact_interface_density = this->num_top_interface_layers > 0 ?
this->top_interface_density : this->bottom_interface_density;
const bool zero_gap_contact_interface = this->num_top_interface_layers > 0 ?
this->zero_gap_interface_top : this->zero_gap_interface_bottom;
if (object_config.support_interface_pattern == smipGrid)
this->contact_fill_pattern = ipGrid;
else if (object_config.support_interface_pattern == smipRectilinearInterlaced)
this->contact_fill_pattern = ipRectilinear;
else
this->contact_fill_pattern =
(object_config.support_interface_pattern == smipAuto && this->zero_gap_interface_top) ||
(object_config.support_interface_pattern == smipAuto && zero_gap_contact_interface) ||
object_config.support_interface_pattern == smipConcentric ?
ipConcentric :
(this->top_interface_density > 0.95 ? ipRectilinear : ipSupportBase);
(contact_interface_density > 0.95 ? ipRectilinear : ipSupportBase);
this->raft_angle_1st_layer = 0.f;
this->raft_angle_base = 0.f;
@@ -188,6 +192,7 @@ struct SupportParameters {
std::numeric_limits<double>::max();
support_style = object_config.support_style;
support_interface_pattern = object_config.support_interface_pattern;
if (support_style != smsDefault) {
if ((support_style == smsSnug || support_style == smsGrid) && is_tree(object_config.support_type)) support_style = smsDefault;
if ((support_style == smsTreeSlim || support_style == smsTreeStrong || support_style == smsTreeHybrid || support_style == smsTreeOrganic) &&
@@ -211,9 +216,9 @@ struct SupportParameters {
bool has_top_contacts;
// Is there at least a bottom contact layer extruded below support base?
bool has_bottom_contacts;
// Number of top interface layers without counting the contact layer.
// User-configured number of top interface layers, including the contact layer.
size_t num_top_interface_layers;
// Number of bottom interface layers without counting the contact layer.
// User-configured number of bottom interface layers, including the contact layer.
size_t num_bottom_interface_layers;
// Number of top base interface layers.
size_t num_top_base_interface_layers;
@@ -235,7 +240,7 @@ struct SupportParameters {
Flow support_material_interface_flow;
// Flow at the bottom interfaces and contacts.
Flow support_material_bottom_interface_flow;
// Flow at raft inteface & contact layers.
// Flow at raft interface & contact layers.
Flow raft_interface_flow;
coordf_t support_extrusion_width;
// Is merging of regions allowed? Could the interface & base support regions be printed with the same extruder?
@@ -262,6 +267,7 @@ struct SupportParameters {
// Density of the base support layers.
coordf_t support_density;
SupportMaterialStyle support_style = smsDefault;
SupportMaterialInterfacePattern support_interface_pattern = smipAuto;
// Pattern of the sparse infill including sparse raft layers.
InfillPattern base_fill_pattern;
@@ -280,9 +286,33 @@ struct SupportParameters {
float raft_angle_base;
float raft_angle_interface;
// Produce a raft interface angle for a given SupportLayer::interface_id()
// Produce a +/-45deg alternating raft interface angle for a given SupportLayer::interface_id().
float raft_interface_angle(size_t interface_id) const
{ return this->raft_angle_interface + ((interface_id & 1) ? float(- M_PI / 4.) : float(+ M_PI / 4.)); }
{ return this->raft_angle_interface + ((interface_id & 1) ? float(- M_PI_4) : float(+ M_PI_4)); }
// Produce support interface angle for a given SupportLayer::interface_id().
// Angle will be shifted/rotated based on interface pattern.
float support_interface_angle(size_t interface_id) const
{
float angle;
switch (this->support_interface_pattern) {
case SupportMaterialInterfacePattern::smipRectilinear:
angle = support_style == SupportMaterialStyle::smsSnug ? this->interface_angle - float(M_PI_4) : this->interface_angle;
break;
case SupportMaterialInterfacePattern::smipRectilinearInterlaced:
angle = this->interface_angle + ((interface_id & 1) ? float(M_PI_4) : float(-M_PI_4));
break;
case SupportMaterialInterfacePattern::smipGrid:
angle = this->base_angle;
break;
default:
angle = this->interface_angle;
break;
}
return angle;
}
bool independent_layer_height = false;
const double thresh_big_overhang = Slic3r::sqr(scale_(10));
@@ -22,7 +22,7 @@ struct Params
: /*max_acceleration(max_acceleration), */raft_layers_count(raft_layers_count), brim_type(brim_type), brim_width(brim_width)
{
if (filament_types.size() > 1) {
BOOST_LOG_TRIVIAL(warning)
BOOST_LOG_TRIVIAL(debug)
<< "SupportSpotsGenerator does not currently handle different materials properly, only first will be used";
}
if (filament_types.empty() || filament_types[0].empty()) {
+1 -1
View File
@@ -469,7 +469,7 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex
});
// 2) Sum over top / bottom ranges.
const bool processing_last_mesh = outline_idx == layer_outline_indices.size();
const bool processing_last_mesh = outline_idx == layer_outline_indices.back();
tbb::parallel_for(tbb::blocked_range<LayerIndex>(data.begin(), data.end()),
[&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius,
xy_distance, z_distance_bottom_layers, z_distance_top_layers, min_resolution = m_min_resolution, &data, processing_last_mesh, &throw_on_cancel]
+40 -54
View File
@@ -1511,7 +1511,9 @@ void TreeSupport::generate_toolpaths()
// ORCA: reset interface Fill state per area group to keep angles deterministic.
filler_interface->fixed_angle = false;
filler_interface->layer_id = size_t(-1);
filler_interface->angle = base_support_angle + M_PI_2; // default interface angle is perpendicular to support angle
filler_Roof1stLayer->fixed_angle = false;
filler_Roof1stLayer->layer_id = size_t(-1);
filler_interface->angle = m_support_params.support_interface_angle(area_group.interface_id);
if (area_group.type != SupportLayer::BaseType) {
// interface
if (layer_id == 0) {
@@ -1537,8 +1539,10 @@ void TreeSupport::generate_toolpaths()
fill_params.density = interface_density;
// Note: spacing means the separation between two lines as if they are tightly extruded
filler_Roof1stLayer->spacing = interface_flow.spacing();
filler_Roof1stLayer->angle = base_support_angle;
filler_Roof1stLayer->angle = m_support_params.support_interface_angle(area_group.interface_id);
fill_params.dont_sort = true;
filler_Roof1stLayer->fixed_angle = (m_object_config->support_interface_pattern == smipRectilinearInterlaced ||
m_object_config->support_interface_pattern == smipRectilinear);
Flow interface_base_flow = interface_as_base ? support_flow : interface_flow;
ExtrusionRole interface_role = interface_as_base ? erSupportMaterial : erSupportMaterialInterface;
// generate a perimeter first to support interface better
@@ -1556,18 +1560,11 @@ void TreeSupport::generate_toolpaths()
fill_params.density = bottom_interface_density;
filler_interface->spacing = interface_flow.spacing();
if (m_object_config->support_interface_pattern == smipGrid) {
filler_interface->angle = base_support_angle;
fill_params.dont_sort = true;
}
if (m_object_config->support_interface_pattern == smipRectilinearInterlaced) {
// ORCA: explicit 0/90 alternation for rectilinear interlaced interfaces.
filler_interface->fixed_angle = true;
filler_interface->angle = base_support_angle + ((area_group.interface_id & 1) * M_PI_2);
fill_params.dont_sort = true;
}
fill_params.dont_sort = (m_object_config->support_interface_pattern == smipGrid ||
m_object_config->support_interface_pattern == smipRectilinearInterlaced);
filler_interface->fixed_angle = (m_object_config->support_interface_pattern == smipRectilinearInterlaced ||
m_object_config->support_interface_pattern == smipRectilinear);
Flow interface_base_flow = interface_as_base ? support_flow : interface_flow;
ExtrusionRole interface_role = interface_as_base ? erSupportMaterial : erSupportMaterialInterface;
@@ -1579,17 +1576,11 @@ void TreeSupport::generate_toolpaths()
fill_params.density = interface_density;
filler_interface->spacing = interface_flow.spacing();
if (m_object_config->support_interface_pattern == smipGrid) {
filler_interface->angle = base_support_angle;
fill_params.dont_sort = true;
}
fill_params.dont_sort = (m_object_config->support_interface_pattern == smipGrid ||
m_object_config->support_interface_pattern == smipRectilinearInterlaced);
if (m_object_config->support_interface_pattern == smipRectilinearInterlaced) {
// ORCA: explicit 0/90 alternation for rectilinear interlaced interfaces.
filler_interface->fixed_angle = true;
filler_interface->angle = base_support_angle + ((area_group.interface_id & 1) * M_PI_2);
fill_params.dont_sort = true;
}
filler_interface->fixed_angle = (m_object_config->support_interface_pattern == smipRectilinearInterlaced ||
m_object_config->support_interface_pattern == smipRectilinear);
Flow interface_base_flow = interface_as_base ? support_flow : interface_flow;
ExtrusionRole interface_role = interface_as_base ? erSupportMaterial : erSupportMaterialInterface;
@@ -2014,6 +2005,9 @@ void TreeSupport::draw_circles()
// generate areas
const coordf_t layer_height = config.layer_height.value;
const size_t top_interface_layers = m_support_params.num_top_interface_layers;
const int top_base_interface_layers = std::min<int>(
int(m_support_params.num_top_base_interface_layers),
top_interface_layers > 0 ? int(top_interface_layers) - 1 : 0);
const size_t bottom_interface_layers = number_of_support_interface_bottom_layers(config);
const double nozzle_diameter = m_object->print()->config().nozzle_diameter.get_at(0);
const coordf_t line_width = config.get_abs_value("support_line_width", nozzle_diameter);
@@ -2054,12 +2048,14 @@ void TreeSupport::draw_circles()
ExPolygons& base_areas = ts_layer->base_areas;
ExPolygons& roof_areas = ts_layer->roof_areas;
ExPolygons roof_base_areas;
ExPolygons& roof_1st_layer = ts_layer->roof_1st_layer;
ExPolygons& floor_areas = ts_layer->floor_areas;
ExPolygons& roof_gap_areas = ts_layer->roof_gap_areas;
coordf_t max_layers_above_base = 0;
coordf_t max_layers_above_roof = 0;
coordf_t max_layers_above_roof1 = 0;
size_t first_base_roof_area = 0;
bool floor_interface_as_base = false;
bool has_circle_node = false;
bool need_extra_wall = false;
@@ -2094,8 +2090,6 @@ void TreeSupport::draw_circles()
break;
const SupportNode& node = *p_node;
// ORCA: Cap top interface height in mm based on per-node support layer height.
const coordf_t top_interface_height = coordf_t(top_interface_layers) * node.height;
ExPolygons area;
// Generate directly from overhang polygon if one of the following is true:
// 1) node is a normal part of hybrid support
@@ -2159,18 +2153,16 @@ void TreeSupport::draw_circles()
if (obj_layer_nr>0 && node.distance_to_top < 0)
append(roof_gap_areas, area);
// ORCA: Roof1stLayer must also fit inside the mm cap.
else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 &&
(node.dist_mm_to_top - this->top_z_distance) < top_interface_height + EPSILON && node.is_sharp_tail==false)
node.is_sharp_tail == false)
{
append(roof_1st_layer, area);
max_layers_above_roof1 = std::max(max_layers_above_roof1, node.dist_mm_to_top);
}
// ORCA: Roof layers must also fit inside the mm cap.
else if (obj_layer_nr > 0 && node.support_roof_layers_below > 1 &&
(node.dist_mm_to_top - this->top_z_distance) < top_interface_height + EPSILON && node.is_sharp_tail == false)
node.is_sharp_tail == false)
{
append(roof_areas, area);
append(node.support_roof_layers_below <= top_base_interface_layers ? roof_base_areas : roof_areas, area);
max_layers_above_roof = std::max(max_layers_above_roof, node.dist_mm_to_top);
}
else
@@ -2184,9 +2176,17 @@ void TreeSupport::draw_circles()
//m_object->print()->set_status(65, (boost::format( _u8L("Support: generate polygons at layer %d")) % layer_nr).str());
// join roof segments
roof_areas = diff_clipped(offset2_ex(roof_areas, line_width_scaled, -line_width_scaled), get_collision(false));
roof_areas = diff_clipped(closing_ex(roof_areas, line_width_scaled), get_collision(false));
roof_areas = intersection_ex(roof_areas, m_machine_border);
roof_1st_layer = diff_clipped(offset2_ex(roof_1st_layer, line_width_scaled, -line_width_scaled), get_collision(false));
roof_base_areas = diff_clipped(closing_ex(roof_base_areas, line_width_scaled), get_collision(false));
roof_base_areas = intersection_ex(roof_base_areas, m_machine_border);
if (!roof_base_areas.empty() && !roof_areas.empty())
roof_base_areas = diff_ex(roof_base_areas,
ClipperUtils::clip_clipper_polygons_with_subject_bbox(roof_areas, get_extents(roof_base_areas)));
first_base_roof_area = roof_areas.size();
append(roof_areas, std::move(roof_base_areas));
roof_1st_layer = diff_clipped(closing_ex(roof_1st_layer, line_width_scaled), get_collision(false));
// roof_1st_layer and roof_areas may intersect, so need to subtract roof_areas from roof_1st_layer
roof_1st_layer = diff_ex(roof_1st_layer, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roof_areas,get_extents(roof_1st_layer)));
@@ -2366,9 +2366,11 @@ void TreeSupport::draw_circles()
area_groups.back().need_infill = overlaps({ expoly }, area_poly);
area_groups.back().need_extra_wall = need_extra_wall && !area_groups.back().need_infill;
}
for (auto& expoly : ts_layer->roof_areas) {
for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
auto &expoly = ts_layer->roof_areas[roof_idx];
//if (area(expoly) < SQ(scale_(1))) continue;
area_groups.emplace_back(&expoly, SupportLayer::RoofType, max_layers_above_roof);
area_groups.back().interface_as_base = roof_idx >= first_base_roof_area;
}
for (auto &expoly : ts_layer->floor_areas) {
//if (area(expoly) < SQ(scale_(1))) continue;
@@ -2378,6 +2380,7 @@ void TreeSupport::draw_circles()
for (auto &expoly : ts_layer->roof_1st_layer) {
//if (area(expoly) < SQ(scale_(1))) continue;
area_groups.emplace_back(&expoly, SupportLayer::Roof1stLayer, max_layers_above_roof1);
area_groups.back().interface_as_base = top_base_interface_layers > 0;
}
for (auto &area_group : area_groups) {
@@ -2406,7 +2409,6 @@ void TreeSupport::draw_circles()
}
});
// ORCA: normalize interface_id sequencing to follow printed interface layers only.
const int top_base_layers = int(m_support_params.num_top_base_interface_layers);
const bool interlaced = m_object_config->support_interface_pattern == smipRectilinearInterlaced;
int roof_interface_id = 0;
int floor_interface_id = 0;
@@ -2425,7 +2427,6 @@ void TreeSupport::draw_circles()
if (area_group.type == SupportLayer::RoofType || area_group.type == SupportLayer::Roof1stLayer) {
if (interlaced)
area_group.interface_id = roof_interface_id;
area_group.interface_as_base = top_base_layers > 0 && roof_interface_id < top_base_layers;
has_roof_interface = true;
} else if (area_group.type == SupportLayer::FloorType) {
if (interlaced)
@@ -2897,7 +2898,7 @@ void TreeSupport::drop_nodes()
node_parent->merged_neighbours.push_front(node_parent == p_node ? neighbour : p_node);
const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position);
SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next,
node_parent->support_roof_layers_below - (node_parent->distance_to_top > 0 ? 1 : 0),
node_parent->support_roof_layers_below - (node_parent->distance_to_top >= 0 ? 1 : 0),
to_buildplate, node_parent, print_z_next, height_next);
get_max_move_dist(next_node);
m_ts_data->m_mutex.lock();
@@ -2949,7 +2950,7 @@ void TreeSupport::drop_nodes()
for(auto& overhang:overhangs_next) {
Point next_pt = overhang.contour.centroid();
SupportNode *next_node = m_ts_data->create_node(next_pt, p_node->distance_to_top + 1, obj_layer_nr_next,
p_node->support_roof_layers_below - (p_node->distance_to_top > 0 ? 1 : 0),
p_node->support_roof_layers_below - (p_node->distance_to_top >= 0 ? 1 : 0),
to_buildplate, p_node, print_z_next, height_next);
next_node->max_move_dist = 0;
next_node->overhang = std::move(overhang);
@@ -3096,7 +3097,7 @@ void TreeSupport::drop_nodes()
auto next_collision = get_collision(0, obj_layer_nr_next);
const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex);
SupportNode * next_node = m_ts_data->create_node(next_layer_vertex, node.distance_to_top + 1, obj_layer_nr_next,
node.support_roof_layers_below - (node.distance_to_top > 0 ? 1 : 0),
node.support_roof_layers_below - (node.distance_to_top >= 0 ? 1 : 0),
to_buildplate, p_node, print_z_next, height_next);
// don't increase radius if next node will collide partially with the object (STUDIO-7883)
to_outside = projection_onto(next_collision, next_node->position);
@@ -3376,21 +3377,6 @@ std::vector<LayerHeightData> TreeSupport::plan_layer_heights()
}
}
// ORCA: Recompute support_roof_layers_below from remaining interface height (independent heights).
const int top_layers = m_object->config().support_interface_top_layers.value;
if (m_support_params.independent_layer_height && top_layers > 0) {
const coordf_t interface_height_mm = coordf_t(top_layers) * m_slicing_params.layer_height;
for (int layer_nr = 0; layer_nr < contact_nodes.size(); layer_nr++) {
if (contact_nodes[layer_nr].empty()) continue;
for (SupportNode *node : contact_nodes[layer_nr]) {
if (node->height <= EPSILON) continue;
const coordf_t remaining_mm = interface_height_mm - (node->dist_mm_to_top - this->top_z_distance);
const int layers_fit = remaining_mm < -EPSILON ? 0 : int(std::floor((remaining_mm + EPSILON) / node->height));
node->support_roof_layers_below = std::min(layers_fit, top_layers);
}
}
}
// log layer_heights
for (size_t i = 0; i < layer_heights.size(); i++) {
//if (layer_heights[i].height > EPSILON)
@@ -3498,7 +3484,7 @@ void TreeSupport::generate_contact_points()
if (force_add || !already_inserted.count(hash_pos)) {
already_inserted.emplace(hash_pos);
bool to_buildplate = true;
size_t roof_layers = add_interface ? (support_roof_layers > 0 ? support_roof_layers - 1 : 0) : 0; // subtract 1 because the contact node itself counts as one layer
size_t roof_layers = add_interface ? support_roof_layers : 0;
// add a new node as a virtual node which acts as the invisible gap between support and object
// distance_to_top=-1: it's virtual
// print_z=object_layer->bottom_z: it directly contacts the bottom
+1 -1
View File
@@ -706,7 +706,7 @@ static std::optional<std::pair<Point, size_t>> polyline_sample_next_point_at_dis
filler->spacing = flow.spacing();
filler->angle = roof ?
//fixme support_layer.interface_id() instead of layer_idx
(support_params.interface_angle + (layer_idx & 1) ? float(- M_PI / 4.) : float(+ M_PI / 4.)) :
(support_params.interface_angle + ((layer_idx & 1) ? float(- M_PI_4) : float(+ M_PI_4))) :
support_params.base_angle;
// ORCA: use top-specific interface density after separating top/bottom settings.
+2 -2
View File
@@ -62,7 +62,7 @@ struct TreeSupportMeshGroupSettings {
this->support_line_width = support_material_flow(&print_object, config.layer_height).scaled_width();
this->support_roof_line_width = support_material_interface_flow(&print_object, config.layer_height).scaled_width();
const int bottom_interface_layers = number_of_support_interface_bottom_layers(config);
this->support_bottom_enable = config.support_interface_top_layers.value > 0 && bottom_interface_layers > 0;
this->support_bottom_enable = bottom_interface_layers > 0;
this->support_bottom_height = this->support_bottom_enable ?
bottom_interface_layers * this->layer_height :
0;
@@ -705,7 +705,7 @@ public:
SupportGeneratorLayersPtr& top_contacts_mutable() { return this->top_contacts; }
public:
// Insert the contact layer and some of the inteface and base interface layers below.
// Insert the contact layer and some of the interface and base interface layers below.
void add_roofs(std::vector<Polygons> &&new_roofs, const size_t insert_layer_idx)
{
if (! new_roofs.empty()) {
+7
View File
@@ -102,6 +102,9 @@ const std::string& var_dir();
// Return a full resource path for a file_name.
std::string var(const std::string &file_name);
// Snap a nozzle diameter to the closest supported value and format it as a string (e.g. 0.4 -> "0.4").
std::string format_diameter_to_str(double diameter, int precision = 1);
// Set a path with various static definition data (for example the initial config bundles).
void set_resources_dir(const std::string &path);
// Return a full path to the resources directory.
@@ -300,6 +303,10 @@ std::string header_gcodeviewer_generated();
// getpid platform wrapper
extern unsigned get_current_pid();
// Per-user id for isolating temp dirs; empty on Windows (its temp dir is already per-user).
std::string per_user_temp_id();
// Per-user temp root under `base`; an empty `user_id` returns `base` unchanged.
std::string per_user_temp_dir(const std::string &base, const std::string &user_id);
// BBS: backup & restore
std::string get_process_name(int pid);

Some files were not shown because too many files have changed in this diff Show More