mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 02:41:17 +00:00
Merge branch 'main' into cad-mainline
This commit is contained in:
+585
-72
@@ -53,6 +53,8 @@ using namespace nlohmann;
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/GCode.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
@@ -71,17 +73,21 @@ using namespace nlohmann;
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "libslic3r/BlacklistedLibraryCheck.hpp"
|
||||
#include "libslic3r/FlushVolCalc.hpp"
|
||||
#include "libslic3r/LayOnFace.hpp"
|
||||
|
||||
#include "libslic3r/Orient.hpp"
|
||||
#include "libslic3r/PNGReadWrite.hpp"
|
||||
#include "libslic3r/ObjColorUtils.hpp"
|
||||
|
||||
#include "OrcaSlicer.hpp"
|
||||
//BBS: add exception handler for win32
|
||||
#include <wx/filename.h>
|
||||
#include <wx/stdpaths.h>
|
||||
//BBS: add exception handler for win32
|
||||
#ifdef WIN32
|
||||
#include "dev-utils/BaseException.h"
|
||||
#endif
|
||||
#include "slic3r/Utils/MeshInspect.hpp"
|
||||
#include "slic3r/Utils/PaintCLI.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/GUI/BitmapCache.hpp"
|
||||
#include "slic3r/GUI/OpenGLManager.hpp"
|
||||
@@ -160,6 +166,7 @@ std::map<int, std::string> cli_errors = {
|
||||
{CLI_FILAMENT_CAN_NOT_MAP, "Some filaments cannot be mapped to correct extruders for multi-extruder Printer."},
|
||||
{CLI_ONLY_ONE_TPU_SUPPORTED, "Not support printing 2 or more TPU filaments."},
|
||||
{CLI_FILAMENTS_NOT_SUPPORTED_BY_EXTRUDER, "Some filaments cannot be printed on the extruder mapped to."},
|
||||
{CLI_MIXED_FILAMENT_INVALID, "A mixed filament is invalid: its components are different filament types, or it has no filament of its own."},
|
||||
{CLI_SLICING_ERROR, "Failed slicing the model. Please verify the slicing of all plates on Orca Slicer before uploading."},
|
||||
{CLI_GCODE_PATH_CONFLICTS, " G-code conflicts detected after slicing. Please make sure the 3mf file can be successfully sliced in the latest Orca Slicer. If the file slices normally in Orca Slicer, try moving the wipe tower further from other models, as we use more conservative parameters for it during upload."},
|
||||
{CLI_GCODE_PATH_IN_UNPRINTABLE_AREA, "Found G-code in unprintable area of multi-extruder printers after slicing. Please make sure the 3mf file can be successfully sliced in the latest Orca Slicer."}
|
||||
@@ -185,6 +192,9 @@ typedef struct _sliced_info {
|
||||
int wall_loops{0};
|
||||
std::vector<std::string> upward_machines;
|
||||
std::vector<std::string> downward_machines;
|
||||
// Structured slicing warnings for result.json, and whether --strict was on.
|
||||
nlohmann::json warnings = nlohmann::json::array();
|
||||
bool strict_mode {false};
|
||||
}sliced_info_t;
|
||||
std::vector<PrintBase::SlicingStatus> g_slicing_warnings;
|
||||
|
||||
@@ -420,6 +430,21 @@ static PrinterTechnology get_printer_technology(const DynamicConfig &config)
|
||||
return(ret);}
|
||||
#endif
|
||||
|
||||
// Records a structured slicing warning so a CI or scripted consumer can branch on
|
||||
// a stable `class` string instead of matching stderr. Warnings are kept on the
|
||||
// run's sliced_info and emitted as the top-level "warnings" array of result.json;
|
||||
// a non-empty array does not by itself mean the run failed. Under --strict a
|
||||
// NON_CRITICAL warning additionally ends the run non-zero.
|
||||
//
|
||||
// result.json is written on Linux only (see the guard in record_exit_reson), so
|
||||
// neither "warnings" nor "strict_mode" reaches Windows or macOS.
|
||||
static void cli_record_warning(sliced_info_t &sliced_info, const std::string &cls,
|
||||
nlohmann::json details = nlohmann::json::object())
|
||||
{
|
||||
details["class"] = cls;
|
||||
sliced_info.warnings.push_back(std::move(details));
|
||||
}
|
||||
|
||||
void record_exit_reson(std::string outputdir, int code, int plate_id, std::string error_message, sliced_info_t& sliced_info, std::map<std::string, std::string> key_values = std::map<std::string, std::string>())
|
||||
{
|
||||
#if defined(__linux__) || defined(__LINUX__)
|
||||
@@ -458,6 +483,9 @@ void record_exit_reson(std::string outputdir, int code, int plate_id, std::strin
|
||||
for (auto& iter: key_values)
|
||||
j[iter.first] = iter.second;
|
||||
|
||||
j["warnings"] = sliced_info.warnings;
|
||||
j["strict_mode"] = sliced_info.strict_mode;
|
||||
|
||||
boost::nowide::ofstream c;
|
||||
c.open(result_file, std::ios::out | std::ios::trunc);
|
||||
c << j.dump(1, '\t') << std::endl;
|
||||
@@ -1377,12 +1405,87 @@ int CLI::run(int argc, char **argv)
|
||||
bool need_skip = (skip_objects.size() > 0)?true:false;
|
||||
long long global_begin_time = 0, global_current_time;
|
||||
sliced_info_t sliced_info;
|
||||
// Read up front so result.json reports it for early failures too.
|
||||
sliced_info.strict_mode = m_config.opt_bool("strict");
|
||||
// --no-check skips the check behind the only NON_CRITICAL warning --strict acts on
|
||||
// (support needed but disabled), from the point it appears among the actions. The pair
|
||||
// would make --strict a no-op or depend on argument order, so refuse it.
|
||||
if (sliced_info.strict_mode && m_config.opt_bool("no_check")) {
|
||||
boost::nowide::cerr << "--strict cannot be combined with --no-check" << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
std::map<std::string, std::string> record_key_values;
|
||||
|
||||
ConfigOptionBool* downward_check_option = m_config.option<ConfigOptionBool>("downward_check");
|
||||
if (downward_check_option)
|
||||
downward_check = downward_check_option->value;
|
||||
|
||||
// --inspect-mesh prints its JSON and exits, so any action that does work of its
|
||||
// own (slicing, exporting) would be skipped without notice. Reject those up front;
|
||||
// only options that merely tune how the input is loaded may come along.
|
||||
if (std::find(m_actions.begin(), m_actions.end(), "inspect_mesh") != m_actions.end()) {
|
||||
static const std::set<std::string> inspect_compatible = { "inspect_mesh", "uptodate", "load_defaultfila", "min_save",
|
||||
"mtcpp", "mstpp", "no_check", "normative_check", "pipe" };
|
||||
for (const std::string &action : m_actions) {
|
||||
if (inspect_compatible.count(action) == 0) {
|
||||
std::string flag = action;
|
||||
std::replace(flag.begin(), flag.end(), '_', '-');
|
||||
boost::nowide::cerr << "--inspect-mesh cannot be combined with --" << flag << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
// Without input there is nothing to inspect; fail rather than print nothing and exit 0.
|
||||
if (m_input_files.empty() && m_config.opt_string("load_assemble_list").empty()) {
|
||||
boost::nowide::cerr << "--inspect-mesh needs an input file or --load-assemble-list" << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
|
||||
// --inspect-paint prints its JSON and exits, so any action that does work of its
|
||||
// own (slicing, exporting) would be skipped without notice. Reject those up front;
|
||||
// only options that merely tune how the input is loaded may come along.
|
||||
if (std::find(m_actions.begin(), m_actions.end(), "inspect_paint") != m_actions.end()) {
|
||||
static const std::set<std::string> inspect_compatible = { "inspect_paint", "uptodate", "load_defaultfila", "min_save",
|
||||
"mtcpp", "mstpp", "no_check", "normative_check", "pipe" };
|
||||
for (const std::string &action : m_actions) {
|
||||
if (inspect_compatible.count(action) == 0) {
|
||||
std::string flag = action;
|
||||
std::replace(flag.begin(), flag.end(), '_', '-');
|
||||
boost::nowide::cerr << "--inspect-paint cannot be combined with --" << flag << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
// Without input there is nothing to inspect; fail rather than print nothing and exit 0.
|
||||
if (m_input_files.empty() && m_config.opt_string("load_assemble_list").empty()) {
|
||||
boost::nowide::cerr << "--inspect-paint needs an input file or --load-assemble-list" << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
|
||||
// --export-settings - writes its JSON to stdout, so reject every action or transform that may write there
|
||||
// too (--info, --help, --orient, slicing and exporting). The allowed ones do nothing when nothing is
|
||||
// sliced or exported.
|
||||
if (std::find(m_actions.begin(), m_actions.end(), "export_settings") != m_actions.end() && m_config.opt_string("export_settings") == "-") {
|
||||
static const std::set<std::string> stdout_compatible = { "export_settings", "uptodate", "load_defaultfila", "min_save",
|
||||
"mtcpp", "mstpp", "no_check", "normative_check", "pipe" };
|
||||
for (const std::vector<std::string> *opt_keys : { &m_actions, &m_transforms }) {
|
||||
for (const std::string &opt_key : *opt_keys) {
|
||||
if (stdout_compatible.count(opt_key) == 0) {
|
||||
std::string flag = opt_key;
|
||||
std::replace(flag.begin(), flag.end(), '_', '-');
|
||||
boost::nowide::cerr << "--export-settings - cannot be combined with --" << flag << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool start_gui = m_actions.empty() && !downward_check;
|
||||
if (start_gui) {
|
||||
BOOST_LOG_TRIVIAL(info) << "no action, start gui directly" << std::endl;
|
||||
@@ -1466,6 +1569,10 @@ int CLI::run(int argc, char **argv)
|
||||
std::vector<std::string> upward_compatible_printers, new_print_compatible_printers, current_print_compatible_printers, current_different_settings;
|
||||
std::vector<std::string> current_filaments_name, current_filaments_system_name, current_inherits_group, current_extruder_variants, new_extruder_variants, current_print_extruder_variants, new_printer_extruder_variants;
|
||||
DynamicPrintConfig load_process_config, load_machine_config;
|
||||
//ORCA: full configs of the "current" (3MF-embedded) process/printer presets, kept so that
|
||||
// compatible_printers_condition can be evaluated for them below. Previously only the
|
||||
// literal compatible_printers list was extracted.
|
||||
DynamicPrintConfig current_process_full_config, current_printer_full_config;
|
||||
bool new_process_config_is_system = true, new_printer_config_is_system = true;
|
||||
std::string pipe_name, makerlab_name, makerlab_version, different_process_setting;
|
||||
const std::vector<std::string> &metadata_name = m_config.option<ConfigOptionStrings>("metadata_name", true)->values;
|
||||
@@ -1925,7 +2032,7 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
boost::nowide::cerr << construct_assemble_list << ": " << e.what() << std::endl;
|
||||
boost::nowide::cerr << "construct_assemble_list: " << e.what() << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_DATA_FILE_ERROR, 0, cli_errors[CLI_DATA_FILE_ERROR], sliced_info);
|
||||
flush_and_exit(CLI_DATA_FILE_ERROR);
|
||||
}
|
||||
@@ -1975,7 +2082,7 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
|
||||
std::unique_ptr<PresetBundle> cli_preset_bundle;
|
||||
auto ensure_cli_preset_bundle = [&cli_preset_bundle, config_substitution_rule](std::string &error) -> PresetBundle * {
|
||||
auto ensure_cli_preset_bundle = [&cli_preset_bundle](std::string &error) -> PresetBundle * {
|
||||
if (cli_preset_bundle)
|
||||
return cli_preset_bundle.get();
|
||||
try {
|
||||
@@ -2002,19 +2109,21 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
};
|
||||
|
||||
auto resolve_preset = [&ensure_cli_preset_bundle, config_substitution_rule](const std::string &file, DynamicPrintConfig &config,
|
||||
// One resolver for the whole run, so presets from the same vendor tree share its load.
|
||||
std::unique_ptr<PresetBundle> system_preset_resolver;
|
||||
auto resolve_preset = [&ensure_cli_preset_bundle, &system_preset_resolver](const std::string &file, DynamicPrintConfig &config,
|
||||
std::string &config_type, const std::string &config_from,
|
||||
bool probe_type, std::string &error) {
|
||||
const auto *inherits = config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS);
|
||||
if (!probe_type && (inherits == nullptr || inherits->value.empty()))
|
||||
return true;
|
||||
|
||||
std::unique_ptr<PresetBundle> source_bundle;
|
||||
PresetBundle *bundle = nullptr;
|
||||
bool allow_source_manifest = false;
|
||||
if (config_from == "system") {
|
||||
source_bundle = std::make_unique<PresetBundle>();
|
||||
bundle = source_bundle.get();
|
||||
if (!system_preset_resolver)
|
||||
system_preset_resolver = std::make_unique<PresetBundle>();
|
||||
bundle = system_preset_resolver.get();
|
||||
allow_source_manifest = true;
|
||||
} else {
|
||||
bundle = ensure_cli_preset_bundle(error);
|
||||
@@ -2046,7 +2155,52 @@ int CLI::run(int argc, char **argv)
|
||||
error, allow_source_manifest);
|
||||
};
|
||||
|
||||
auto load_config_file = [config_substitution_rule, &resolve_preset](const std::string& file, DynamicPrintConfig& config, std::string& config_type,
|
||||
//ORCA: list the keys a user preset overrides relative to its system parent, for the
|
||||
// `different_settings_to_system` column of an exported 3MF. Without it the CLI
|
||||
// writes an empty column, so re-opening a CLI-exported project in the GUI shows
|
||||
// spurious "unsaved changes" and can revert inherited process/filament/machine
|
||||
// values to system defaults.
|
||||
//
|
||||
// The parent comes from the preset bundle that inherits resolution already builds,
|
||||
// so this adds no extra loading. Returns "" whenever the parent cannot be resolved,
|
||||
// which is exactly the previous behaviour.
|
||||
auto cli_different_settings = [&ensure_cli_preset_bundle](const DynamicPrintConfig &resolved,
|
||||
const std::string &parent_name,
|
||||
Preset::Type type) -> std::string {
|
||||
if (parent_name.empty())
|
||||
return std::string();
|
||||
std::string error;
|
||||
PresetBundle *bundle = ensure_cli_preset_bundle(error);
|
||||
if (bundle == nullptr) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "CLI: no preset bundle for different_settings_to_system: " << error;
|
||||
return std::string();
|
||||
}
|
||||
const PresetCollection *collection = nullptr;
|
||||
switch (type) {
|
||||
case Preset::TYPE_PRINT: collection = &bundle->prints; break;
|
||||
case Preset::TYPE_FILAMENT: collection = &bundle->filaments; break;
|
||||
case Preset::TYPE_PRINTER: collection = &bundle->printers; break;
|
||||
default: return std::string();
|
||||
}
|
||||
const Preset *parent = collection->find_preset2(parent_name, true);
|
||||
if (parent == nullptr) {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("CLI: parent preset '%1%' not found; leaving different_settings_to_system empty")%parent_name;
|
||||
return std::string();
|
||||
}
|
||||
std::vector<std::string> keys = resolved.diff(parent->config);
|
||||
//ORCA: preset metadata, not user-tunable settings. compatible_printers /
|
||||
// compatible_prints have their own tracking columns and would double-count.
|
||||
keys.erase(std::remove_if(keys.begin(), keys.end(), [](const std::string &k) {
|
||||
return k == "inherits" || k == "compatible_printers" || k == "compatible_prints"
|
||||
|| k == "compatible_printers_condition" || k == "compatible_prints_condition"
|
||||
|| k == "print_settings_id" || k == "filament_settings_id" || k == "printer_settings_id";
|
||||
}),
|
||||
keys.end());
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("CLI: %1% overrides vs parent '%2%'")%keys.size()%parent_name;
|
||||
return Slic3r::escape_strings_cstyle(keys);
|
||||
};
|
||||
|
||||
auto load_config_file = [&resolve_preset](const std::string& file, DynamicPrintConfig& config, std::string& config_type,
|
||||
std::string& config_name, std::string& filament_id, std::string& config_from) {
|
||||
if (! boost::filesystem::exists(file)) {
|
||||
boost::nowide::cerr << __FUNCTION__<< ": can not find setting file: " << file << std::endl;
|
||||
@@ -2635,6 +2789,8 @@ int CLI::run(int argc, char **argv)
|
||||
flush_and_exit(ret);
|
||||
}
|
||||
upward_compatible_printers = config.option<ConfigOptionStrings>("upward_compatible_machine", true)->values;
|
||||
//ORCA: keep the full config so compatible_printers_condition can be evaluated against it below
|
||||
current_printer_full_config = std::move(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2657,6 +2813,8 @@ int CLI::run(int argc, char **argv)
|
||||
flush_and_exit(ret);
|
||||
}
|
||||
current_print_compatible_printers = config.option<ConfigOptionStrings>("compatible_printers", true)->values;
|
||||
//ORCA: keep the full config so compatible_printers_condition can be evaluated against it below
|
||||
current_process_full_config = std::move(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2675,46 +2833,88 @@ int CLI::run(int argc, char **argv)
|
||||
for (int index = 0; index < upward_compatible_printers.size(); index++) {
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("index %1%, upward_compatible_printers %2%")%index %upward_compatible_printers[index];
|
||||
}
|
||||
//ORCA: Replace the four manual equality-loop checks below with is_compatible_with_printer(), the
|
||||
// same helper the GUI uses, which also evaluates compatible_printers_condition. Process
|
||||
// profiles that declare compatibility via condition only -- leaving compatible_printers
|
||||
// empty -- were always reported incompatible by the literal-name match, so a CLI slice with
|
||||
// such a preset exited with CLI_PROCESS_NOT_COMPATIBLE (-17) even though the GUI accepts the
|
||||
// same pair. Behaviour is unchanged where an explicit list exists: is_compatible_with_printer
|
||||
// does the same name match, and returns true when both list and condition are empty (which
|
||||
// matches the "old 3mf, no compatible printers" path below).
|
||||
auto check_compat = [](const DynamicPrintConfig &process_cfg,
|
||||
const DynamicPrintConfig &printer_cfg,
|
||||
const std::string &printer_name) -> bool {
|
||||
return is_compatible_with_printer(process_cfg, Preset::TYPE_PRINT, printer_cfg, printer_name);
|
||||
};
|
||||
|
||||
//ORCA: a 3MF's project config does not carry compatible_printers / compatible_printers_condition.
|
||||
// PresetBundle::construct_full_config() erases both and re-emits them as
|
||||
// print_compatible_printers and compatible_machine_expression_group; they are renamed back
|
||||
// only on the PresetBundle load path, which the CLI does not take. Feeding the project config
|
||||
// to the check as-is therefore presents no list and no condition, and
|
||||
// is_compatible_with_printer() reads that as "no constraint" and accepts every printer.
|
||||
// Translate the two keys back. Index 0 of the expression group is the print preset -- the
|
||||
// group is filled print, filaments, printer (PresetBundle.cpp).
|
||||
// The raw keys win whenever they carry something. A project the CLI exported itself has the
|
||||
// real compatible_printers_condition AND an all-empty compatible_machine_expression_group,
|
||||
// so copying the group's first entry unconditionally would overwrite a valid condition with
|
||||
// "" and accept every printer. The renamed keys are only a fallback, and an empty value is
|
||||
// never written over a real one.
|
||||
auto cli_process_compat_config = [](const DynamicPrintConfig &project_cfg) -> DynamicPrintConfig {
|
||||
DynamicPrintConfig cfg = project_cfg;
|
||||
const auto *raw_list = project_cfg.option<ConfigOptionStrings>("compatible_printers");
|
||||
const auto *list = project_cfg.option<ConfigOptionStrings>("print_compatible_printers");
|
||||
if ((raw_list == nullptr || raw_list->values.empty()) && list != nullptr && !list->values.empty())
|
||||
cfg.set_key_value("compatible_printers", new ConfigOptionStrings(list->values));
|
||||
const auto *raw_cond = project_cfg.option<ConfigOptionString>("compatible_printers_condition");
|
||||
const auto *group = project_cfg.option<ConfigOptionStrings>("compatible_machine_expression_group");
|
||||
if ((raw_cond == nullptr || raw_cond->value.empty()) && group != nullptr && !group->values.empty() &&
|
||||
!group->values.front().empty())
|
||||
cfg.set_key_value("compatible_printers_condition", new ConfigOptionString(group->values.front()));
|
||||
return cfg;
|
||||
};
|
||||
if (!new_printer_name.empty()) {
|
||||
if (!new_process_name.empty()) {
|
||||
for (int index = 0; index < new_print_compatible_printers.size(); index++) {
|
||||
if (new_print_compatible_printers[index] == new_printer_system_name) {
|
||||
process_compatible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//new process + new printer: both configs came from --load-settings
|
||||
process_compatible = check_compat(load_process_config, load_machine_config, new_printer_system_name);
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("new printer %1%, inherited from %2%, new process %3%, inherited from %4% ,compatible %5%")
|
||||
%new_printer_name %new_printer_system_name %new_process_name %new_process_system_name %process_compatible;
|
||||
}
|
||||
else {
|
||||
for (int index = 0; index < current_print_compatible_printers.size(); index++) {
|
||||
if (current_print_compatible_printers[index] == new_printer_system_name) {
|
||||
process_compatible = true;
|
||||
break;
|
||||
}
|
||||
//3MF-embedded process vs new printer. current_process_full_config is only populated from
|
||||
//profiles/BBL/process_full/, so for every other vendor fall back to the 3MF's own project
|
||||
//config in m_print_config, with its renamed compatibility keys translated back (see
|
||||
//cli_process_compat_config above). Without this a 3MF built from a condition-only process
|
||||
//is rejected when re-sliced with the very printer it was made for.
|
||||
{
|
||||
//ORCA: profiles/BBL/{process,machine}_full/ are gitignored and not generated in-tree,
|
||||
// so current_*_full_config is always empty and this fallback is the only live path.
|
||||
const DynamicPrintConfig process_cfg = current_process_full_config.empty()
|
||||
? cli_process_compat_config(m_print_config)
|
||||
: current_process_full_config;
|
||||
process_compatible = check_compat(process_cfg, load_machine_config, new_printer_system_name);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("new printer %1%, inherited from %2%, old process %3%, inherited from %4% ,compatible %5%")
|
||||
%new_printer_name %new_printer_system_name %current_process_name %current_process_system_name %process_compatible;
|
||||
}
|
||||
}
|
||||
else if (!new_process_name.empty()) {
|
||||
for (int index = 0; index < new_print_compatible_printers.size(); index++) {
|
||||
if (new_print_compatible_printers[index] == current_printer_system_name) {
|
||||
process_compatible = true;
|
||||
break;
|
||||
}
|
||||
//new process vs 3MF-embedded printer. As above, current_printer_full_config only resolves for
|
||||
//BBL profiles; otherwise evaluate against the 3MF's own project config in m_print_config, which
|
||||
//holds the embedded printer's printer_notes / nozzle_diameter.
|
||||
{
|
||||
const DynamicPrintConfig &printer_cfg = current_printer_full_config.empty() ? m_print_config : current_printer_full_config;
|
||||
process_compatible = check_compat(load_process_config, printer_cfg, current_printer_system_name);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("old printer %1%, inherited from %2%, new process %3%, inherited from %4% ,compatible %5%")
|
||||
%current_printer_name %current_printer_system_name %new_process_name %new_process_system_name %process_compatible;
|
||||
}
|
||||
else {
|
||||
//check the compatible of old printer&&process
|
||||
for (int index = 0; index < current_print_compatible_printers.size(); index++) {
|
||||
if (current_print_compatible_printers[index] == current_printer_system_name) {
|
||||
process_compatible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//both sides 3MF-embedded (pure reprocess)
|
||||
if (!current_process_full_config.empty() && !current_printer_full_config.empty())
|
||||
process_compatible = check_compat(current_process_full_config, current_printer_full_config, current_printer_system_name);
|
||||
else
|
||||
process_compatible = std::find(current_print_compatible_printers.begin(), current_print_compatible_printers.end(), current_printer_system_name) != current_print_compatible_printers.end();
|
||||
if (!process_compatible && current_print_compatible_printers.empty())
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("old 3mf, no compatible printers, set to compatible");
|
||||
@@ -2937,8 +3137,10 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
else {
|
||||
//todo: support user machine preset's different settings
|
||||
different_settings[filament_count+1] = "";
|
||||
//ORCA: was a //todo — compute the user's overrides instead of writing an empty column.
|
||||
different_settings[filament_count+1] = new_printer_config_is_system
|
||||
? std::string()
|
||||
: cli_different_settings(load_machine_config, new_printer_system_name, Preset::TYPE_PRINTER);
|
||||
if (new_printer_config_is_system)
|
||||
inherits_group[filament_count+1] = "";
|
||||
else
|
||||
@@ -3080,8 +3282,14 @@ int CLI::run(int argc, char **argv)
|
||||
print_compatible_printers = std::move(current_print_compatible_printers);
|
||||
}
|
||||
else {
|
||||
//todo: support system process preset
|
||||
different_settings[0] = "";
|
||||
//ORCA: was a //todo. Prefer a value the loaded JSON already carried, otherwise
|
||||
// compute the overrides against the system parent.
|
||||
if (!different_process_setting.empty())
|
||||
different_settings[0] = different_process_setting;
|
||||
else
|
||||
different_settings[0] = new_process_config_is_system
|
||||
? std::string()
|
||||
: cli_different_settings(load_process_config, new_process_system_name, Preset::TYPE_PRINT);
|
||||
if (new_process_config_is_system)
|
||||
inherits_group[0] = "";
|
||||
else
|
||||
@@ -3268,6 +3476,16 @@ int CLI::run(int argc, char **argv)
|
||||
int filament_index = load_filaments_index[index];
|
||||
std::vector<std::string> different_keys;
|
||||
|
||||
//ORCA: diff before load_default_gcodes_to_config, the way the process and machine
|
||||
// slots above already do. That call materialises absent gcode keys via
|
||||
// option(..., true), and DynamicConfig::diff only compares keys present in
|
||||
// both configs -- so a gcode key the leaf did not carry would go from "not
|
||||
// compared" to "compared as empty against the parent" and land in the column
|
||||
// as an override the user never made.
|
||||
std::string filament_different_settings;
|
||||
if (load_filament_count > 0)
|
||||
filament_different_settings = cli_different_settings(config, load_filaments_inherit[index], Preset::TYPE_FILAMENT);
|
||||
|
||||
load_default_gcodes_to_config(config, Preset::TYPE_FILAMENT);
|
||||
|
||||
if (load_filament_count > 0) {
|
||||
@@ -3279,8 +3497,8 @@ int CLI::run(int argc, char **argv)
|
||||
opt_filament_settings->set_at(filament_name_setting, filament_index-1, 0);
|
||||
config.erase("filament_settings_id");
|
||||
|
||||
//todo: update different settings of filaments
|
||||
different_settings[filament_index] = "";
|
||||
//ORCA: was a //todo — same treatment as process/machine above.
|
||||
different_settings[filament_index] = filament_different_settings;
|
||||
inherits_group[filament_index] = load_filaments_inherit[index];
|
||||
}
|
||||
else {
|
||||
@@ -3585,6 +3803,15 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
// A mixed slot never reaches a nozzle, so its row and column stay empty, as in the GUI.
|
||||
// Command line options are not merged into m_print_config yet, so they win here.
|
||||
const ConfigOptionBools *is_mixed_opt = m_extra_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
if (!is_mixed_opt)
|
||||
is_mixed_opt = m_print_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
auto is_mixed_slot = [is_mixed_opt](int idx) {
|
||||
return is_mixed_opt && idx < static_cast<int>(is_mixed_opt->values.size()) && is_mixed_opt->values[idx];
|
||||
};
|
||||
|
||||
for (size_t nozzle_id = 0; nozzle_id < new_extruder_count; ++nozzle_id) {
|
||||
std::vector<double> flush_vol_mtx = get_flush_volumes_matrix(flush_vol_matrix, nozzle_id, new_extruder_count);
|
||||
for (int from_idx = 0; from_idx < project_filament_count; from_idx++) {
|
||||
@@ -3594,7 +3821,7 @@ int CLI::run(int argc, char **argv)
|
||||
bool is_from_support = filament_is_support->get_at(from_idx);
|
||||
for (int to_idx = 0; to_idx < project_filament_count; to_idx++) {
|
||||
bool is_to_support = filament_is_support->get_at(to_idx);
|
||||
if (from_idx == to_idx) {
|
||||
if (from_idx == to_idx || is_mixed_slot(from_idx) || is_mixed_slot(to_idx)) {
|
||||
flush_vol_mtx[project_filament_count * from_idx + to_idx] = 0.f;
|
||||
} else {
|
||||
int flushing_volume = 0;
|
||||
@@ -3731,12 +3958,113 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
//ORCA: settings passed on the command line (--sparse-infill-density 25% ...) override the loaded
|
||||
// presets right here, so they belong in different_settings_to_system just as a preset
|
||||
// override does. Without them re-opening the exported project in the GUI shows nothing
|
||||
// modified and reverts those values to the system presets'.
|
||||
//
|
||||
// The keys come from m_config, not m_extra_config: read_cli() puts only what the user typed
|
||||
// into m_config (setup() adds nothing but CLI-own defaults), whereas the CLI writes its own
|
||||
// values into m_extra_config. Only keys whose value the override actually changed are
|
||||
// recorded -- a typed value equal to the loaded one modifies nothing -- and each lands in
|
||||
// the column(s) whose preset type owns it: [0] process, [1..n-2] filaments, [n-1] printer.
|
||||
//
|
||||
// "Changed" is judged the way the value is read: a list is compared entry by entry with a
|
||||
// missing entry read as the first, as get_at() does -- so --nozzle-temperature 245 against
|
||||
// 245,245,245 is no change, although the two serialize differently.
|
||||
//
|
||||
// A key the loaded config does not carry at all is always recorded, even if the typed value
|
||||
// equals the built-in default. On reopen the GUI restores an unlisted key from the SYSTEM
|
||||
// preset, which need not match that default: a 3MF written before an option existed leaves
|
||||
// it absent here, and --sparse-infill-density 20% (the default) against a Prusa system 15%
|
||||
// would otherwise go unrecorded and be reverted. Over-recording is cosmetic; under-recording
|
||||
// loses the value.
|
||||
std::map<std::string, std::unique_ptr<ConfigOption>> cli_override_before;
|
||||
for (const std::string &key : m_config.keys()) {
|
||||
if (!m_extra_config.has(key))
|
||||
continue;
|
||||
const ConfigOption *loaded = m_print_config.option(key);
|
||||
cli_override_before[key].reset(loaded != nullptr ? loaded->clone() : nullptr); // null: always recorded
|
||||
}
|
||||
|
||||
// Apply command line options to a more specific DynamicPrintConfig which provides normalize()
|
||||
// (command line options override --load files)
|
||||
m_print_config.apply(m_extra_config, true);
|
||||
|
||||
if (!cli_override_before.empty()) {
|
||||
std::vector<std::string> &columns = m_print_config.option<ConfigOptionStrings>("different_settings_to_system", true)->values;
|
||||
auto owned_by = [](const std::vector<std::string> &options, const std::string &key) {
|
||||
return std::find(options.begin(), options.end(), key) != options.end();
|
||||
};
|
||||
auto add_to_column = [&columns](size_t index, const std::string &key) {
|
||||
std::vector<std::string> keys;
|
||||
Slic3r::unescape_strings_cstyle(columns[index], keys);
|
||||
if (std::find(keys.begin(), keys.end(), key) == keys.end()) {
|
||||
keys.push_back(key);
|
||||
columns[index] = Slic3r::escape_strings_cstyle(keys);
|
||||
}
|
||||
};
|
||||
auto same_value = [](const ConfigOption *a, const ConfigOption *b) {
|
||||
if (a == nullptr || b == nullptr)
|
||||
return false;
|
||||
const auto *va = dynamic_cast<const ConfigOptionVectorBase *>(a);
|
||||
const auto *vb = dynamic_cast<const ConfigOptionVectorBase *>(b);
|
||||
if (va == nullptr || vb == nullptr)
|
||||
return va == vb && a->serialize() == b->serialize();
|
||||
const std::vector<std::string> ea = va->vserialize(), eb = vb->vserialize();
|
||||
if (ea.empty() || eb.empty())
|
||||
return ea.empty() && eb.empty();
|
||||
for (size_t i = 0; i < std::max(ea.size(), eb.size()); ++i)
|
||||
if (ea[i < ea.size() ? i : 0] != eb[i < eb.size() ? i : 0])
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
//ORCA: always true after the resize to filament_count + 2 above, and nothing in between can
|
||||
// shrink the column vector -- different_settings_to_system is not a CLI option. Kept as
|
||||
// a check rather than an assert: release builds compile asserts out, so an assert would
|
||||
// protect nothing, while a build with _GLIBCXX_ASSERTIONS would abort on columns[0].
|
||||
if (columns.size() >= 2) {
|
||||
for (const auto &[key, before] : cli_override_before) {
|
||||
if (same_value(before.get(), m_print_config.option(key)))
|
||||
continue;
|
||||
bool recorded = false;
|
||||
if (owned_by(Preset::print_options(), key)) {
|
||||
add_to_column(0, key);
|
||||
recorded = true;
|
||||
}
|
||||
if (owned_by(Preset::filament_options(), key)) {
|
||||
for (size_t i = 1; i + 1 < columns.size(); ++i)
|
||||
add_to_column(i, key);
|
||||
recorded = true;
|
||||
}
|
||||
if (owned_by(Preset::printer_options(), key)) {
|
||||
add_to_column(columns.size() - 1, key);
|
||||
recorded = true;
|
||||
}
|
||||
if (recorded)
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("CLI: override %1% recorded in different_settings_to_system") % key;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Normalizing after importing the 3MFs / AMFs
|
||||
m_print_config.normalize_fdm();
|
||||
|
||||
// A mixed slot is virtual but still needs a filament entry of its own. Without one, feature
|
||||
// filament ids aimed at it fall outside the filament count, are reset to the first filament
|
||||
// and the model silently prints in a single colour.
|
||||
if (const auto *is_mixed_opt = m_print_config.option<ConfigOptionBools>("filament_is_mixed")) {
|
||||
const auto &is_mixed = is_mixed_opt->values;
|
||||
for (size_t slot = static_cast<size_t>(std::max(filament_count, 0)); slot < is_mixed.size(); ++slot) {
|
||||
if (!is_mixed[slot])
|
||||
continue;
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("mixed filament slot %1% has no filament of its own, only %2% filaments are loaded; "
|
||||
"load one filament per slot, including each mixed one")
|
||||
% (slot + 1) % filament_count;
|
||||
record_exit_reson(outfile_dir, CLI_MIXED_FILAMENT_INVALID, 0, cli_errors[CLI_MIXED_FILAMENT_INVALID], sliced_info);
|
||||
flush_and_exit(CLI_MIXED_FILAMENT_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
m_print_config.option<ConfigOptionEnum<PrinterTechnology>>("printer_technology", true)->value = printer_technology;
|
||||
|
||||
bool has_wipe_tower_position = m_print_config.option<ConfigOptionFloats>("wipe_tower_x") && m_print_config.option<ConfigOptionFloats>("wipe_tower_y");
|
||||
@@ -3791,6 +4119,15 @@ int CLI::run(int argc, char **argv)
|
||||
bool is_smooth_timelapse = false;
|
||||
if (enable_timelapse && timelapse_type_opt && (timelapse_type_opt->getInt() == TimelapseType::tlSmooth))
|
||||
is_smooth_timelapse = true;
|
||||
// A mixed filament swaps between its components every layer, so it needs the tower even when
|
||||
// every loaded preset is the same.
|
||||
if (disable_wipe_tower_after_mapping) {
|
||||
if (const auto *is_mixed_opt = m_print_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
is_mixed_opt && has_any_mixed_filament(is_mixed_opt->values)) {
|
||||
disable_wipe_tower_after_mapping = false;
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%, set disable_wipe_tower_after_mapping back to false due to a mixed filament")%__LINE__;
|
||||
}
|
||||
}
|
||||
if (disable_wipe_tower_after_mapping) {
|
||||
if (is_smooth_timelapse)
|
||||
{
|
||||
@@ -4015,7 +4352,7 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
};
|
||||
|
||||
auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse, new_extruder_count](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
|
||||
auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
|
||||
plate_obj_size_info.obj_bbox= plate->get_objects_bounding_box();
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%, object bbox: min {%2%, %3%, %4%} - max {%5%, %6%, %7%}")
|
||||
%(plate_index+1) %plate_obj_size_info.obj_bbox.min.x() % plate_obj_size_info.obj_bbox.min.y() % plate_obj_size_info.obj_bbox.min.z() %plate_obj_size_info.obj_bbox.max.x() % plate_obj_size_info.obj_bbox.max.y() % plate_obj_size_info.obj_bbox.max.z();
|
||||
@@ -4059,22 +4396,13 @@ int CLI::run(int argc, char **argv)
|
||||
plate_obj_size_info.wipe_x = wipe_x_option->get_at(plate_index);
|
||||
plate_obj_size_info.wipe_y = wipe_y_option->get_at(plate_index);
|
||||
|
||||
ConfigOptionFloat* width_option = print_config.option<ConfigOptionFloat>("prime_tower_width", true);
|
||||
plate_obj_size_info.wipe_width = width_option->value;
|
||||
// Body and brim from one estimate: resolving an auto (-1) brim against a different
|
||||
// height would size the two halves of the same tower from two different objects.
|
||||
const WipeTowerFootprint footprint = plate->estimate_wipe_tower_footprint(print_config, filaments_cnt);
|
||||
float brim_width = float(footprint.brim_width);
|
||||
|
||||
ConfigOptionFloat* brim_width_option = print_config.option<ConfigOptionFloat>("prime_tower_brim_width", true);
|
||||
float brim_width = brim_width_option->value;
|
||||
if (brim_width < 0) brim_width = WipeTower::get_auto_brim_by_height((float)plate_obj_size_info.obj_bbox.max.z());
|
||||
|
||||
ConfigOptionFloat* volume_option = print_config.option<ConfigOptionFloat>("prime_volume", true);
|
||||
float wipe_volume = volume_option->value;
|
||||
|
||||
const ConfigOptionBool * wrapping_detection = print_config.option<ConfigOptionBool>("enable_wrapping_detection");
|
||||
bool enable_wrapping = (wrapping_detection != nullptr) && wrapping_detection->value;
|
||||
|
||||
Vec3d wipe_tower_size = plate->estimate_wipe_tower_size(print_config, plate_obj_size_info.wipe_width, wipe_volume, new_extruder_count, filaments_cnt, false, enable_wrapping);
|
||||
plate_obj_size_info.wipe_width = wipe_tower_size(0);
|
||||
plate_obj_size_info.wipe_depth = wipe_tower_size(1);
|
||||
plate_obj_size_info.wipe_width = footprint.width;
|
||||
plate_obj_size_info.wipe_depth = footprint.depth;
|
||||
|
||||
Vec3d origin = plate->get_origin();
|
||||
Vec3d start(origin(0) + plate_obj_size_info.wipe_x - brim_width, origin(1) + plate_obj_size_info.wipe_y, 0.f);
|
||||
@@ -4562,6 +4890,64 @@ int CLI::run(int argc, char **argv)
|
||||
for (auto &o : model.objects)
|
||||
// this affects volumes:
|
||||
o->rotate(Geometry::deg2rad(m_config.opt_float(opt_key)), Y);
|
||||
} else if (opt_key == "ground_largest_face" || opt_key == "ground_face_normal" || opt_key == "ground_face_point") {
|
||||
// Each instance is laid on one of its lay-on-face planes, which are computed from the current part
|
||||
// transformations, so the rotations given before this option are respected. A direction or point is in
|
||||
// object coordinates, so it names the same face for every instance of an object.
|
||||
std::function<int(const std::vector<LayOnFacePlane>&, const Transform3d&)> pick;
|
||||
if (opt_key == "ground_largest_face") {
|
||||
if (m_config.opt_bool(opt_key))
|
||||
pick = [](const std::vector<LayOnFacePlane>& planes, const Transform3d&) { return find_largest_plane(planes); };
|
||||
} else {
|
||||
// Only options given on the command line reach this loop, so an empty value is malformed input too.
|
||||
const std::string& value = m_config.opt_string(opt_key);
|
||||
Vec3d v;
|
||||
int consumed = 0;
|
||||
if (sscanf(value.c_str(), "%lf,%lf,%lf%n", &v.x(), &v.y(), &v.z(), &consumed) != 3 || consumed != int(value.size()) ||
|
||||
!v.allFinite() || (opt_key == "ground_face_normal" && v.norm() < EPSILON)) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("Invalid params: %1% expects three comma-separated numbers, got \"%2%\"") % opt_key % value;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
if (opt_key == "ground_face_normal")
|
||||
pick = [v](const std::vector<LayOnFacePlane>& planes, const Transform3d&) { return find_plane_by_normal(planes, v); };
|
||||
else
|
||||
pick = [v](const std::vector<LayOnFacePlane>& planes, const Transform3d& inst_matrix) {
|
||||
return find_plane_at_point(planes, inst_matrix, v, 0.01);
|
||||
};
|
||||
}
|
||||
if (pick) {
|
||||
size_t laid = 0, missed = 0;
|
||||
for (auto& model : m_models) {
|
||||
model.add_default_instances();
|
||||
for (ModelObject* o : model.objects)
|
||||
for (size_t i = 0; i < o->instances.size(); ++i) {
|
||||
const Transform3d inst_matrix = o->instances[i]->get_matrix_no_offset();
|
||||
const std::vector<LayOnFacePlane> planes = lay_on_face_planes(*o, inst_matrix);
|
||||
if (planes.empty()) {
|
||||
// Small or smooth parts (e.g. a sphere) have no face to rest on; the gizmo offers none either.
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: object %2% has no face large enough to lay on, left as it is") % opt_key % o->name;
|
||||
continue;
|
||||
}
|
||||
const int idx = pick(planes, inst_matrix);
|
||||
if (idx < 0) {
|
||||
// Only a point can miss: with several objects it usually belongs to one of them.
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: no face of object %2% contains the point, left as it is") % opt_key % o->name;
|
||||
++missed;
|
||||
continue;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: object %2% instance %3% laid on the %4% mm2 face with normal %5%")
|
||||
% opt_key % o->name % i % planes[idx].area % planes[idx].normal.transpose();
|
||||
lay_on_face(*o, i, planes[idx].normal);
|
||||
++laid;
|
||||
}
|
||||
}
|
||||
if (laid == 0 && missed > 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("Invalid params: %1%: no face of any object contains the point") % opt_key;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
} else if (opt_key == "scale") {
|
||||
float ratio = m_config.opt_float(opt_key);
|
||||
if (ratio <= 0.f) {
|
||||
@@ -4835,13 +5221,16 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
if (!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1)||(enable_wrapping_detect && !current_wrapping_exclude_area.empty()))
|
||||
if ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1))||(enable_wrapping_detect && !current_wrapping_exclude_area.empty()))
|
||||
{
|
||||
//prepare the wipe tower
|
||||
int plate_count = partplate_list.get_plate_count();
|
||||
|
||||
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
|
||||
const float tower_brim_width = m_print_config.option<ConfigOptionFloat>("prime_tower_width", true)->value;
|
||||
// This margin only pre-adjusts the default away from the near edges;
|
||||
// estimate_wipe_tower_polygon below computes the real clamped position.
|
||||
float tower_brim_width = m_print_config.option<ConfigOptionFloat>("prime_tower_brim_width", true)->value;
|
||||
if (tower_brim_width < 0.f) tower_brim_width = 8.f; // auto: object heights unknown here, 8 mm is the auto cap
|
||||
const float tower_margin = WIPE_TOWER_MARGIN + tower_brim_width;
|
||||
|
||||
// set the default position, the same with print config(left top)
|
||||
@@ -4875,7 +5264,7 @@ int CLI::run(int argc, char **argv)
|
||||
wipe_y_option->set_at(&wt_y_opt, i, 0);
|
||||
|
||||
Vec3d wipe_tower_size, wipe_tower_pos;
|
||||
ArrangePolygon wipe_tower_ap = cur_plate->estimate_wipe_tower_polygon(m_print_config, i, wipe_tower_pos, wipe_tower_size, new_extruder_count, assemble_plate.filaments_count, true);
|
||||
ArrangePolygon wipe_tower_ap = cur_plate->estimate_wipe_tower_polygon(m_print_config, i, wipe_tower_pos, wipe_tower_size, assemble_plate.filaments_count, true);
|
||||
|
||||
//update the new wp position
|
||||
wt_x_opt.value = wipe_tower_pos(0);
|
||||
@@ -5118,7 +5507,7 @@ int CLI::run(int argc, char **argv)
|
||||
//skip this object due to be locked in plate
|
||||
ap.itemid = locked_aps.size();
|
||||
locked_aps.emplace_back(ap);
|
||||
boost::nowide::cout <<__FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5138,7 +5527,10 @@ int CLI::run(int argc, char **argv)
|
||||
int extruder_size = used_filament_set.size();
|
||||
|
||||
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
|
||||
const float tower_brim_width = m_print_config.option<ConfigOptionFloat>("prime_tower_width", true)->value;
|
||||
// This margin only pre-adjusts the default away from the near edges;
|
||||
// estimate_wipe_tower_polygon below computes the real clamped position.
|
||||
float tower_brim_width = m_print_config.option<ConfigOptionFloat>("prime_tower_brim_width", true)->value;
|
||||
if (tower_brim_width < 0.f) tower_brim_width = 8.f; // auto: object heights unknown here, 8 mm is the auto cap
|
||||
const float tower_margin = WIPE_TOWER_MARGIN + tower_brim_width;
|
||||
// set the default position, the same with print config(left top)
|
||||
float x = WIPE_TOWER_DEFAULT_X_POS;
|
||||
@@ -5175,7 +5567,7 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
|
||||
Vec3d wipe_tower_size, wipe_tower_pos;
|
||||
ArrangePolygon wipe_tower_ap = partplate_list.get_plate(plate_index_valid)->estimate_wipe_tower_polygon(m_print_config, plate_index_valid, wipe_tower_pos, wipe_tower_size, new_extruder_count, extruder_size, true);
|
||||
ArrangePolygon wipe_tower_ap = partplate_list.get_plate(plate_index_valid)->estimate_wipe_tower_polygon(m_print_config, plate_index_valid, wipe_tower_pos, wipe_tower_size, extruder_size, true);
|
||||
|
||||
//update the new wp position
|
||||
if (bedid < plate_count) {
|
||||
@@ -5276,22 +5668,16 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
//float depth = v * (filaments_cnt - 1) / (layer_height * w);
|
||||
|
||||
const ConfigOptionBool *wrapping_detection = m_print_config.option<ConfigOptionBool>("enable_wrapping_detection");
|
||||
bool enable_wrapping = (wrapping_detection != nullptr) && wrapping_detection->value;
|
||||
|
||||
Vec3d wipe_tower_size = cur_plate->estimate_wipe_tower_size(m_print_config, w, v, new_extruder_count, filaments_cnt, false, enable_wrapping);
|
||||
const WipeTowerFootprint footprint = cur_plate->estimate_wipe_tower_footprint(m_print_config, filaments_cnt);
|
||||
Vec3d wipe_tower_size(footprint.width, footprint.depth, footprint.height);
|
||||
Vec3d plate_origin = cur_plate->get_origin();
|
||||
int plate_width, plate_depth;
|
||||
double plate_height;
|
||||
partplate_list.get_plate_size(plate_width, plate_depth, plate_height);
|
||||
float depth = wipe_tower_size(1);
|
||||
float margin = 15.f, wp_brim_width = 0.f;
|
||||
ConfigOption *wipe_tower_brim_width_opt = m_print_config.option("prime_tower_brim_width");
|
||||
if (wipe_tower_brim_width_opt ) {
|
||||
wp_brim_width = wipe_tower_brim_width_opt->getFloat();
|
||||
if (wp_brim_width < 0) wp_brim_width = WipeTower::get_auto_brim_by_height((float) wipe_tower_size.z());
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%")%wp_brim_width;
|
||||
}
|
||||
// Brim already resolved against the height the body was sized from.
|
||||
float margin = 15.f, wp_brim_width = float(footprint.brim_width);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%")%wp_brim_width;
|
||||
w = wipe_tower_size(0);
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: x=%1%, y=%2%, width=%3%, depth=%4%, angle=%5%, prime_volume=%6%, filaments_cnt=%7%, layer_height=%8%, plate_width=%9%, plate_depth=%10%")
|
||||
@@ -5710,13 +6096,64 @@ int CLI::run(int argc, char **argv)
|
||||
//FIXME check for mixing the FFF / SLA parameters.
|
||||
// or better save fff_print_config vs. sla_print_config
|
||||
//m_print_config.save(m_config.opt_string("save"));
|
||||
m_print_config.save_to_json(m_config.opt_string(opt_key), std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION));
|
||||
const std::string &settings_file = m_config.opt_string(opt_key);
|
||||
if (settings_file == "-")
|
||||
m_print_config.save_to_json(boost::nowide::cout, "project_settings", "project", SoftFever_VERSION, /*replace_invalid_utf8=*/true);
|
||||
else
|
||||
m_print_config.save_to_json(settings_file, std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION));
|
||||
} else if (opt_key == "info") {
|
||||
// --info works on unrepaired model
|
||||
for (Model &model : m_models) {
|
||||
model.add_default_instances();
|
||||
model.print_info();
|
||||
}
|
||||
} else if (opt_key == "inspect_mesh") {
|
||||
// Machine-readable alternative to --info. Registered as an action so it satisfies the
|
||||
// "needs an action" check and bypasses the GUI fallback, then exits once the JSON is out.
|
||||
for (Model &model : m_models) {
|
||||
model.add_default_instances();
|
||||
Slic3r::MeshInspect::inspect_to_json(model, m_input_files, boost::nowide::cout);
|
||||
}
|
||||
boost::nowide::cout.flush();
|
||||
// Conflicting actions were rejected before loading. Finish like the end of run().
|
||||
// flush_and_exit() is not usable here: it prints "found error ..." to stdout,
|
||||
// which would corrupt the JSON.
|
||||
#if defined(__linux__) || defined(__LINUX__)
|
||||
if (g_cli_callback_mgr.is_started()) {
|
||||
PrintBase::SlicingStatus slicing_status{100, "All done, Success"};
|
||||
cli_status_callback(slicing_status);
|
||||
}
|
||||
g_cli_callback_mgr.stop();
|
||||
#endif
|
||||
for (Model &m : m_models)
|
||||
m.remove_backup_path_if_exist();
|
||||
record_exit_reson(outfile_dir, CLI_SUCCESS, plate_to_slice, cli_errors[CLI_SUCCESS], sliced_info);
|
||||
boost::nowide::cerr.flush();
|
||||
return CLI_SUCCESS;
|
||||
} else if (opt_key == "inspect_paint") {
|
||||
// --inspect-paint — read the per-facet enforcer/blocker/extruder/
|
||||
// fuzzy state from the loaded model and emit a JSON summary.
|
||||
// Machine-readable alternative to opening the paint gizmos.
|
||||
for (Model &model : m_models) {
|
||||
model.add_default_instances();
|
||||
Slic3r::PaintCLI::inspect_to_json(model, m_input_files, boost::nowide::cout);
|
||||
}
|
||||
boost::nowide::cout.flush();
|
||||
// The tooltip promises "then exit"; conflicting actions were rejected before
|
||||
// loading. Finish like the end of run(). flush_and_exit() is not usable here:
|
||||
// it prints "found error ..." to stdout, which would corrupt the JSON.
|
||||
#if defined(__linux__) || defined(__LINUX__)
|
||||
if (g_cli_callback_mgr.is_started()) {
|
||||
PrintBase::SlicingStatus slicing_status{100, "All done, Success"};
|
||||
cli_status_callback(slicing_status);
|
||||
}
|
||||
g_cli_callback_mgr.stop();
|
||||
#endif
|
||||
for (Model &m : m_models)
|
||||
m.remove_backup_path_if_exist();
|
||||
record_exit_reson(outfile_dir, CLI_SUCCESS, plate_to_slice, cli_errors[CLI_SUCCESS], sliced_info);
|
||||
boost::nowide::cerr.flush();
|
||||
return CLI_SUCCESS;
|
||||
} else if (opt_key == "uptodate") {
|
||||
//already processed before
|
||||
} else if (opt_key == "min_save") {
|
||||
@@ -5757,6 +6194,8 @@ int CLI::run(int argc, char **argv)
|
||||
export_3mf_file = m_config.opt_string(opt_key);
|
||||
}else if(opt_key=="no_check"){
|
||||
no_check = m_config.opt_bool(opt_key);
|
||||
}else if(opt_key=="strict"){
|
||||
//already read into sliced_info at the start of run()
|
||||
//} else if (opt_key == "export_gcode" || opt_key == "export_sla" || opt_key == "slice") {
|
||||
} else if (opt_key == "normative_check") {
|
||||
//already processed before
|
||||
@@ -5807,6 +6246,34 @@ int CLI::run(int argc, char **argv)
|
||||
//Print fff_print;
|
||||
std::vector<size_t> plate_triangle_counts(partplate_list.get_plate_count(), 0);
|
||||
|
||||
// The stored (or default) tower position may not fit the tower these plates
|
||||
// need, and no CLI placement site runs on a plain slice - mirror the GUI's
|
||||
// reload clamp and fit every plate's tower into the printable area first.
|
||||
if (m_print_config.option<ConfigOptionBool>("enable_prime_tower", true)->value) {
|
||||
for (int index = 0; index < partplate_list.get_plate_count(); index++) {
|
||||
if ((plate_to_slice != 0) && (plate_to_slice != (index + 1)))
|
||||
continue;
|
||||
Slic3r::GUI::PartPlate *plate = partplate_list.get_plate(index);
|
||||
// Printing by object disables the tower only with more than one instance.
|
||||
bool is_seq_print = false;
|
||||
get_print_sequence(plate, m_print_config, is_seq_print);
|
||||
if (is_seq_print && plate->printable_instance_size() > 1)
|
||||
continue;
|
||||
// An empty estimate is a plate that prints no tower (one filament and
|
||||
// neither smooth timelapse, wrapping detection nor a raft).
|
||||
Vec3d wt_pos, wt_size;
|
||||
plate->estimate_wipe_tower_polygon(m_print_config, index, wt_pos, wt_size);
|
||||
if (wt_size(0) < EPSILON || wt_size(1) < EPSILON)
|
||||
continue;
|
||||
ConfigOptionFloat wt_x_opt((float) wt_pos(0));
|
||||
ConfigOptionFloat wt_y_opt((float) wt_pos(1));
|
||||
m_print_config.option<ConfigOptionFloats>("wipe_tower_x", true)->set_at(&wt_x_opt, index, 0);
|
||||
m_print_config.option<ConfigOptionFloats>("wipe_tower_y", true)->set_at(&wt_y_opt, index, 0);
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%: wipe tower clamped to {%2%, %3%}, size {%4%, %5%}")
|
||||
% (index + 1) % wt_pos(0) % wt_pos(1) % wt_size(0) % wt_size(1);
|
||||
}
|
||||
}
|
||||
|
||||
while(!finished)
|
||||
{
|
||||
//BBS: slice every partplate one by one
|
||||
@@ -5978,6 +6445,36 @@ int CLI::run(int argc, char **argv)
|
||||
flush_and_exit(CLI_ONLY_ONE_TPU_SUPPORTED);
|
||||
}
|
||||
|
||||
// Same type gate as the GUI's Sidebar::has_broken_mixed_filament: refuse a plate that uses a
|
||||
// mixed slot whose components are different filament types. Missing or out-of-range
|
||||
// components never get here, validate() already rejects them for the whole project.
|
||||
const auto *is_mixed_opt = m_print_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
const auto *components_opt = m_print_config.option<ConfigOptionStrings>("filament_mixed_components");
|
||||
if (is_mixed_opt && components_opt && has_any_mixed_filament(is_mixed_opt->values)) {
|
||||
const auto &is_mixed = is_mixed_opt->values;
|
||||
const auto &components = components_opt->values;
|
||||
const size_t num_physical = static_cast<size_t>(filament_count) - static_cast<size_t>(std::count(is_mixed.begin(), is_mixed.end(), true));
|
||||
std::vector<std::string> physical_types(num_physical);
|
||||
for (size_t f_index = 0; f_index < num_physical; ++f_index) {
|
||||
std::string displayed_type;
|
||||
physical_types[f_index] = m_print_config.get_filament_type(displayed_type, static_cast<int>(f_index));
|
||||
if (physical_types[f_index].empty())
|
||||
physical_types[f_index] = "PLA";
|
||||
}
|
||||
const std::vector<size_t> mismatched_slots = check_mixed_filament_type_consistency(is_mixed, components, physical_types);
|
||||
// plate_filaments has mixed slots expanded to their components; the gate needs the slots.
|
||||
const std::vector<int> plate_slots = mismatched_slots.empty() ? std::vector<int>() :
|
||||
part_plate->get_extruders_under_cli(true, m_print_config, false);
|
||||
for (size_t slot : mismatched_slots) {
|
||||
if (std::find(plate_slots.begin(), plate_slots.end(), static_cast<int>(slot) + 1) == plate_slots.end())
|
||||
continue;
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("plate %1%: mixed filament %2% mixes components of different filament types")
|
||||
% (index + 1) % (slot + 1);
|
||||
record_exit_reson(outfile_dir, CLI_MIXED_FILAMENT_INVALID, index + 1, cli_errors[CLI_MIXED_FILAMENT_INVALID], sliced_info);
|
||||
flush_and_exit(CLI_MIXED_FILAMENT_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
if (new_extruder_count > 1) {
|
||||
std::vector<std::vector<int>> unprintable_filament_vec;
|
||||
for (const std::set<int>& filamnt_ids : unprintable_filament_ids) {
|
||||
@@ -6407,6 +6904,15 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
if (status.warning_level == PrintStateBase::WarningLevel::NON_CRITICAL) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "plate "<< index+1<< ": found NON_CRITICAL slicing warnings: "<<status.text <<std::endl;
|
||||
// Always record for AI/CI consumers; under --strict, elevate to a
|
||||
// non-zero exit so scripted pipelines don't ship a "warning OK" slice.
|
||||
cli_record_warning(sliced_info, "slicing_warning_non_critical",
|
||||
nlohmann::json{{"plate_id", index+1}, {"text", status.text}});
|
||||
if (sliced_info.strict_mode) {
|
||||
sliced_info.sliced_plates.push_back(sliced_plate_info);
|
||||
record_exit_reson(outfile_dir, CLI_SLICING_ERROR, index+1, cli_errors[CLI_SLICING_ERROR], sliced_info);
|
||||
flush_and_exit(CLI_SLICING_ERROR);
|
||||
}
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("plate %1%: found slicing warnings: %2%, no_check=%3%")%(index+1) %status.text %no_check;
|
||||
@@ -7430,6 +7936,13 @@ bool CLI::setup(int argc, char **argv)
|
||||
this->print_help();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Orca: resolve here, while the process is still in the directory the user invoked it from.
|
||||
// GUI_App's constructor moves the working directory to <data_dir>/log, long before the GUI
|
||||
// opens these files in post_init(), and a relative path would then resolve against that.
|
||||
for (std::string &input_file : m_input_files)
|
||||
input_file = resolve_cli_input_path(input_file);
|
||||
|
||||
// Parse actions and transform options.
|
||||
for (auto const &opt_key : opt_order) {
|
||||
if (cli_actions_config_def.has(opt_key))
|
||||
|
||||
@@ -297,7 +297,7 @@ int wmain(int argc, wchar_t **argv)
|
||||
// printf("Loading Slic3r library: %S\n", path_to_slic3r);
|
||||
HINSTANCE hInstance_Slic3r = LoadLibraryExW(path_to_slic3r, nullptr, 0);
|
||||
if (hInstance_Slic3r == nullptr) {
|
||||
printf("OrcaSlicer.dll was not loaded, error=%d\n", GetLastError());
|
||||
printf("OrcaSlicer.dll was not loaded, error=%lu\n", GetLastError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
#include <boost/nowide/cstdio.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include "stackwalker.h"
|
||||
#include "StackWalker.h"
|
||||
#include <eh.h>
|
||||
|
||||
class CBaseException : public CStackWalker
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
#define NANOSVGRAST_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvgrast.h"
|
||||
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/GCode.hpp"
|
||||
#include "libslic3r/GCode/WipeTower.hpp"
|
||||
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
@@ -116,15 +120,45 @@ Vec2d printable_area_center(const DynamicPrintConfig &cfg)
|
||||
return 0.5 * (lo + hi);
|
||||
}
|
||||
|
||||
// Put the prime tower where the GUI and CLI would before slicing. The config default (x 15, y 220)
|
||||
// lies off any bed shallower than the tower, and generation rejects an off-plate tower instead of
|
||||
// exporting it. Beside the centred cube, clear of the edge exclusion strips some beds carry, then
|
||||
// pulled inside the printable outline by the tower's own estimated footprint, with a few mm of
|
||||
// clearance so the conflict checker never sees the two touch.
|
||||
void place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d ¢er)
|
||||
{
|
||||
const auto *area = cfg.option<ConfigOptionPoints>("printable_area");
|
||||
if (area == nullptr || area->values.size() < 3)
|
||||
return;
|
||||
const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(cfg, resolve_wipe_tower_type(cfg), {0, 1}, cfg.opt_float("layer_height"), 10.);
|
||||
if (footprint.depth < EPSILON)
|
||||
return;
|
||||
const double margin = WIPE_TOWER_MARGIN + footprint.brim_width;
|
||||
// The position is the tower's own origin; a rotated tower extends from it in another
|
||||
// direction, so place the rotated box's extents rather than the origin.
|
||||
Slic3r::Polygon box({Point::new_scale(0., 0.), Point::new_scale(footprint.width, 0.), Point::new_scale(footprint.width, footprint.depth), Point::new_scale(0., footprint.depth)});
|
||||
box.rotate(Geometry::deg2rad(cfg.opt_float("wipe_tower_rotation_angle")));
|
||||
const BoundingBox local = get_extents(box);
|
||||
const Vec2d lo = unscale(local.min);
|
||||
const Vec2d size = unscale(local.max) - lo;
|
||||
Vec2d pos(center.x() + 5. + margin + 5. - lo.x(), center.y() - size.y() / 2. - lo.y());
|
||||
box.translate(Point::new_scale(pos.x(), pos.y()));
|
||||
const Vec2f move = WipeTower::move_box_inside_polygon(get_extents(box), Polygons{Polygon::new_scale(area->values)}, scaled<coord_t>(margin));
|
||||
pos += move.cast<double>();
|
||||
cfg.option<ConfigOptionFloats>("wipe_tower_x", true)->values = {pos.x()};
|
||||
cfg.option<ConfigOptionFloats>("wipe_tower_y", true)->values = {pos.y()};
|
||||
}
|
||||
|
||||
// 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)
|
||||
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl)
|
||||
{
|
||||
const Vec2d center = printable_area_center(cfg);
|
||||
place_wipe_tower(cfg, center);
|
||||
TriangleMesh m = make_cube(10, 10, 10);
|
||||
m.translate(float(center.x() - 5.), float(center.y() - 5.), 0.f);
|
||||
|
||||
|
||||
@@ -364,7 +364,7 @@ void CStackWalker::GetModuleInformation(LPMODULE_INFO pmi)
|
||||
|
||||
if (dwInfoSize > 0)
|
||||
{
|
||||
LPVOID lpData = new byte[dwInfoSize];
|
||||
byte *lpData = new byte[dwInfoSize];
|
||||
ZeroMemory(lpData, dwInfoSize * sizeof(byte));
|
||||
|
||||
if (GetFileVersionInfo(pmi->szModulePath, dwHandle, dwInfoSize, lpData) > 0 )
|
||||
@@ -425,7 +425,7 @@ LPSTACKINFO CStackWalker::StackWalker(HANDLE hThread, const CONTEXT* context)
|
||||
else
|
||||
c = *context;
|
||||
|
||||
STACKFRAME64 sf = {0};
|
||||
STACKFRAME64 sf = {};
|
||||
DWORD imageType;
|
||||
|
||||
//intel X86
|
||||
|
||||
@@ -49,7 +49,7 @@ SplittedLine split_line(const PathType& path, const ExPolygons& clip, bool close
|
||||
|
||||
// Convert the input path into an open ZPath
|
||||
ClipperZUtils::ZPath p;
|
||||
p.reserve(path.size() + closed ? 1 : 0);
|
||||
p.reserve(path.size() + (closed ? 1 : 0));
|
||||
ClipperLib_Z::cInt z = 0;
|
||||
for (const auto& point : path) {
|
||||
p.emplace_back(point.x(), point.y(), z);
|
||||
|
||||
@@ -42,6 +42,9 @@ namespace Slic3r {
|
||||
|
||||
static const std::string VERSION_CHECK_URL = "https://check-version.orcaslicer.com/latest";
|
||||
static const std::string PROFILE_UPDATE_URL = "https://check-version.orcaslicer.com/profile";
|
||||
|
||||
constexpr const char* CONFIG_ORCA_UPDATER_URL = "orca_updater_url";
|
||||
|
||||
static const std::string MODELS_STR = "models";
|
||||
|
||||
const std::string AppConfig::SECTION_FILAMENTS = "filaments";
|
||||
@@ -655,6 +658,11 @@ void AppConfig::set_defaults()
|
||||
set_bool("use_printer_agents", false);
|
||||
}
|
||||
|
||||
if (get("enable_ota").empty())
|
||||
{
|
||||
set_bool("enable_ota", false);
|
||||
}
|
||||
|
||||
// Remove legacy window positions/sizes
|
||||
erase("app", "main_frame_maximized");
|
||||
erase("app", "main_frame_pos");
|
||||
@@ -1835,7 +1843,10 @@ std::string AppConfig::version_check_url() const
|
||||
|
||||
std::string AppConfig::profile_update_url() const
|
||||
{
|
||||
return PROFILE_UPDATE_URL;
|
||||
std::string orca_updater_url = get(CONFIG_ORCA_UPDATER_URL);
|
||||
if (orca_updater_url.empty())
|
||||
return PROFILE_UPDATE_URL;
|
||||
return orca_updater_url;
|
||||
}
|
||||
|
||||
bool AppConfig::exists()
|
||||
|
||||
@@ -265,6 +265,8 @@ set(lisbslic3r_sources
|
||||
GCode/SmallAreaInfillFlowCompensator.hpp
|
||||
GCode/SpiralVase.cpp
|
||||
GCode/SpiralVase.hpp
|
||||
GCode/WipePathHelpers.cpp
|
||||
GCode/WipePathHelpers.hpp
|
||||
GCode/ThumbnailData.cpp
|
||||
GCode/ThumbnailData.hpp
|
||||
GCode/Thumbnails.cpp
|
||||
@@ -277,6 +279,8 @@ set(lisbslic3r_sources
|
||||
GCode/WipeTower2.hpp
|
||||
GCode/WipeTower.cpp
|
||||
GCode/WipeTower.hpp
|
||||
GCode/WipeTowerEstimate.cpp
|
||||
GCode/WipeTowerEstimate.hpp
|
||||
GCodeWriter.cpp
|
||||
GCodeWriter.hpp
|
||||
Geometry/ArcWelder.hpp
|
||||
@@ -305,6 +309,8 @@ set(lisbslic3r_sources
|
||||
Layer.cpp
|
||||
Layer.hpp
|
||||
LayerRegion.cpp
|
||||
LayOnFace.cpp
|
||||
LayOnFace.hpp
|
||||
libslic3r.cpp
|
||||
libslic3r.h
|
||||
Line.cpp
|
||||
@@ -375,6 +381,8 @@ set(lisbslic3r_sources
|
||||
Preset.hpp
|
||||
PrincipalComponents2D.cpp
|
||||
PrincipalComponents2D.hpp
|
||||
PublishSettings.cpp
|
||||
PublishSettings.hpp
|
||||
PrintApply.cpp
|
||||
PrintBase.cpp
|
||||
PrintBase.hpp
|
||||
@@ -582,6 +590,12 @@ if (_opts)
|
||||
target_compile_options(libslic3r_cgal PRIVATE "${_opts_bad}")
|
||||
endif()
|
||||
|
||||
if (IS_CLANG_CL)
|
||||
# CGAL passes /fp:strict /fp:except-. clang-cl reports the second as overriding part of
|
||||
# the first; the settings cc1 receives are the same ones MSVC produces from that pair.
|
||||
target_compile_options(libslic3r_cgal PRIVATE -Wno-overriding-option)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs)
|
||||
|
||||
if (MSVC AND "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # 32 bit MSVC workaround
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <algorithm>
|
||||
#include <assert.h>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <regex>
|
||||
@@ -1515,6 +1516,19 @@ std::optional<PluginCapabilityRef> parse_capability_ref(const std::string& value
|
||||
|
||||
//BBS: add json support
|
||||
void ConfigBase::save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const
|
||||
{
|
||||
// Serialize first: if that throws (invalid UTF-8), the existing file stays untouched.
|
||||
std::ostringstream ss;
|
||||
this->save_to_json(ss, name, from, version);
|
||||
boost::nowide::ofstream c;
|
||||
c.open(file, std::ios::out | std::ios::trunc);
|
||||
c << ss.str();
|
||||
c.close();
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
|
||||
}
|
||||
|
||||
void ConfigBase::save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8) const
|
||||
{
|
||||
json j;
|
||||
//record the headers
|
||||
@@ -1561,12 +1575,7 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
|
||||
j["plugins"] = unique_refs;
|
||||
}
|
||||
|
||||
boost::nowide::ofstream c;
|
||||
c.open(file, std::ios::out | std::ios::trunc);
|
||||
c << j.dump(1, '\t') << std::endl;
|
||||
c.close();
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
|
||||
os << j.dump(1, '\t', false, replace_invalid_utf8 ? json::error_handler_t::replace : json::error_handler_t::strict) << std::endl;
|
||||
}
|
||||
|
||||
void ConfigBase::save(const std::string &file) const
|
||||
|
||||
@@ -1006,6 +1006,7 @@ public:
|
||||
int getInt() const override { return this->value; }
|
||||
void setInt(int val) override { this->value = val; }
|
||||
ConfigOption* clone() const override { return new ConfigOptionInt(*this); }
|
||||
using ConfigOptionSingle<int>::operator==;
|
||||
bool operator==(const ConfigOptionInt &rhs) const throw() { return this->value == rhs.value; }
|
||||
|
||||
std::string serialize() const override
|
||||
@@ -1048,6 +1049,7 @@ public:
|
||||
ConfigOptionType type() const override { return static_type(); }
|
||||
ConfigOption* clone() const override { return new ConfigOptionIntsTempl(*this); }
|
||||
ConfigOptionIntsTempl& operator= (const ConfigOption *opt) { this->set(opt); return *this; }
|
||||
using ConfigOptionVector<int>::operator==;
|
||||
bool operator==(const ConfigOptionIntsTempl &rhs) const throw() { return this->values == rhs.values; }
|
||||
bool operator< (const ConfigOptionIntsTempl &rhs) const throw() { return this->values < rhs.values; }
|
||||
// Could a special "nil" value be stored inside the vector, indicating undefined value?
|
||||
@@ -1137,6 +1139,7 @@ public:
|
||||
ConfigOptionType type() const override { return static_type(); }
|
||||
ConfigOption* clone() const override { return new ConfigOptionString(*this); }
|
||||
ConfigOptionString& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
|
||||
using ConfigOptionSingle<std::string>::operator==;
|
||||
bool operator==(const ConfigOptionString &rhs) const throw() { return this->value == rhs.value; }
|
||||
bool operator< (const ConfigOptionString &rhs) const throw() { return this->value < rhs.value; }
|
||||
bool empty() const { return this->value.empty(); }
|
||||
@@ -1171,6 +1174,7 @@ public:
|
||||
ConfigOptionType type() const override { return static_type(); }
|
||||
ConfigOption* clone() const override { return new ConfigOptionStrings(*this); }
|
||||
ConfigOptionStrings& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
|
||||
using ConfigOptionVector<std::string>::operator==;
|
||||
bool operator==(const ConfigOptionStrings &rhs) const throw() { return this->values == rhs.values; }
|
||||
bool operator< (const ConfigOptionStrings &rhs) const throw() { return this->values < rhs.values; }
|
||||
bool is_nil(size_t) const override { return false; }
|
||||
@@ -1215,6 +1219,7 @@ public:
|
||||
ConfigOptionType type() const override { return static_type(); }
|
||||
ConfigOption* clone() const override { return new ConfigOptionPercent(*this); }
|
||||
ConfigOptionPercent& operator= (const ConfigOption *opt) { this->set(opt); return *this; }
|
||||
using ConfigOptionFloat::operator==;
|
||||
bool operator==(const ConfigOptionPercent &rhs) const throw() { return this->value == rhs.value; }
|
||||
bool operator< (const ConfigOptionPercent &rhs) const throw() { return this->value < rhs.value; }
|
||||
|
||||
@@ -1257,6 +1262,7 @@ public:
|
||||
ConfigOptionType type() const override { return static_type(); }
|
||||
ConfigOption* clone() const override { return new ConfigOptionPercentsTempl(*this); }
|
||||
ConfigOptionPercentsTempl& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
|
||||
using ConfigOptionFloatsTempl<NULLABLE>::operator==;
|
||||
bool operator==(const ConfigOptionPercentsTempl &rhs) const throw() { return ConfigOptionFloatsTempl<NULLABLE>::vectors_equal(this->values, rhs.values); }
|
||||
bool operator< (const ConfigOptionPercentsTempl &rhs) const throw() { return ConfigOptionFloatsTempl<NULLABLE>::vectors_lower(this->values, rhs.values); }
|
||||
|
||||
@@ -1502,6 +1508,7 @@ public:
|
||||
ConfigOptionType type() const override { return static_type(); }
|
||||
ConfigOption* clone() const override { return new ConfigOptionPoint(*this); }
|
||||
ConfigOptionPoint& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
|
||||
using ConfigOptionSingle<Vec2d>::operator==;
|
||||
bool operator==(const ConfigOptionPoint &rhs) const throw() { return this->value == rhs.value; }
|
||||
bool operator< (const ConfigOptionPoint &rhs) const throw() { return this->value < rhs.value; }
|
||||
|
||||
@@ -1539,6 +1546,7 @@ public:
|
||||
ConfigOptionType type() const override { return static_type(); }
|
||||
ConfigOption* clone() const override { return new ConfigOptionPoints(*this); }
|
||||
ConfigOptionPoints& operator= (const ConfigOption *opt) { this->set(opt); return *this; }
|
||||
using ConfigOptionVector<Vec2d>::operator==;
|
||||
bool operator==(const ConfigOptionPoints &rhs) const throw() { return this->values == rhs.values; }
|
||||
bool operator< (const ConfigOptionPoints &rhs) const throw()
|
||||
{ return std::lexicographical_compare(this->values.begin(), this->values.end(), rhs.values.begin(), rhs.values.end(), [](const auto &l, const auto &r){ return l < r; }); }
|
||||
@@ -1617,6 +1625,7 @@ public:
|
||||
ConfigOptionType type() const override { return static_type(); }
|
||||
ConfigOption* clone() const override { return new ConfigOptionPoint3(*this); }
|
||||
ConfigOptionPoint3& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
|
||||
using ConfigOptionSingle<Vec3d>::operator==;
|
||||
bool operator==(const ConfigOptionPoint3 &rhs) const throw() { return this->value == rhs.value; }
|
||||
bool operator< (const ConfigOptionPoint3 &rhs) const throw()
|
||||
{ return this->value.x() < rhs.value.x() || (this->value.x() == rhs.value.x() && (this->value.y() < rhs.value.y() || (this->value.y() == rhs.value.y() && this->value.z() < rhs.value.z()))); }
|
||||
@@ -1860,6 +1869,7 @@ public:
|
||||
bool getBool() const override { return this->value; }
|
||||
ConfigOption* clone() const override { return new ConfigOptionBool(*this); }
|
||||
ConfigOptionBool& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
|
||||
using ConfigOptionSingle<bool>::operator==;
|
||||
bool operator==(const ConfigOptionBool &rhs) const throw() { return this->value == rhs.value; }
|
||||
bool operator< (const ConfigOptionBool &rhs) const throw() { return int(this->value) < int(rhs.value); }
|
||||
|
||||
@@ -1911,6 +1921,7 @@ public:
|
||||
ConfigOptionType type() const override { return static_type(); }
|
||||
ConfigOption* clone() const override { return new ConfigOptionBoolsTempl(*this); }
|
||||
ConfigOptionBoolsTempl& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
|
||||
using ConfigOptionVector<unsigned char>::operator==;
|
||||
bool operator==(const ConfigOptionBoolsTempl &rhs) const throw() { return this->values == rhs.values; }
|
||||
bool operator< (const ConfigOptionBoolsTempl &rhs) const throw() { return this->values < rhs.values; }
|
||||
// Could a special "nil" value be stored inside the vector, indicating undefined value?
|
||||
@@ -2163,6 +2174,7 @@ public:
|
||||
ConfigOptionEnumsGenericTempl& operator= (const ConfigOption* opt) { this->set(opt); return *this; }
|
||||
bool operator< (const ConfigOptionInts& rhs) const throw() { return this->values < rhs.values; }
|
||||
|
||||
using ConfigOptionInts::operator==;
|
||||
bool operator==(const ConfigOptionInts& rhs) const
|
||||
{
|
||||
if (rhs.type() != this->type())
|
||||
@@ -2813,6 +2825,9 @@ public:
|
||||
|
||||
//BBS: add json support
|
||||
void save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const;
|
||||
// Same document, written to a stream. Invalid UTF-8 in a string value throws nlohmann's type_error unless
|
||||
// replace_invalid_utf8 is set, which writes U+FFFD instead (for callers such as stdout with no handler).
|
||||
void save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8 = false) const;
|
||||
|
||||
// Rebuild the in-memory "plugins" manifest (the "name;uuid;capability" references the plugin
|
||||
// dispatchers consume) from the plugin-backed options via the registered resolver. save_to_json()
|
||||
|
||||
@@ -968,10 +968,10 @@ EmbossStyles Emboss::get_font_list_by_register() {
|
||||
}
|
||||
|
||||
// TODO: Fix global function
|
||||
bool CALLBACK EnumFamCallBack(LPLOGFONT lplf,
|
||||
LPNEWTEXTMETRIC lpntm,
|
||||
DWORD FontType,
|
||||
LPVOID aFontList)
|
||||
int CALLBACK EnumFamCallBack(const LOGFONT *lplf,
|
||||
const TEXTMETRIC *lpntm,
|
||||
DWORD FontType,
|
||||
LPARAM aFontList)
|
||||
{
|
||||
std::vector<std::wstring> *fontList =
|
||||
(std::vector<std::wstring> *) (aFontList);
|
||||
@@ -988,7 +988,7 @@ EmbossStyles Emboss::get_font_list_by_enumeration() {
|
||||
|
||||
HDC hDC = GetDC(NULL);
|
||||
std::vector<std::wstring> font_names;
|
||||
EnumFontFamilies(hDC, (LPCTSTR) NULL, (FONTENUMPROC) EnumFamCallBack,
|
||||
EnumFontFamilies(hDC, (LPCTSTR) NULL, EnumFamCallBack,
|
||||
(LPARAM) &font_names);
|
||||
|
||||
EmbossStyles font_list;
|
||||
|
||||
@@ -454,6 +454,10 @@ class ExtrusionLoop : public ExtrusionEntity
|
||||
{
|
||||
public:
|
||||
ExtrusionPaths paths;
|
||||
// ORCA: Set on a loop extruded entirely in mid air and out of reach of the layer below: it has
|
||||
// nothing to lean on until this layer is bridged, so the G-code writer holds it back until the
|
||||
// infill is down. See defer_unsupported_loops() in PerimeterGenerator.cpp.
|
||||
bool print_after_infill = false;
|
||||
|
||||
ExtrusionLoop(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {}
|
||||
ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) {}
|
||||
|
||||
+59
-52
@@ -11,7 +11,7 @@
|
||||
|
||||
#include "AABBTreeLines.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "FillBase.hpp"
|
||||
#include "Fill.hpp"
|
||||
#include "FillRectilinear.hpp"
|
||||
#include "FillLightning.hpp"
|
||||
#include "FillConcentricInternal.hpp"
|
||||
@@ -1234,6 +1234,33 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
return surface_fills;
|
||||
}
|
||||
|
||||
// Orca: Anchors and printed infill must share the same body origin. Keep the choice
|
||||
// here so per-model surface centering and separated sparse infill cannot drift apart.
|
||||
static BoundingBox infill_bounding_box(const Layer &layer, const SurfaceFill &fill, const ExPolygon &expoly, BoundingBox bbox)
|
||||
{
|
||||
const auto ¶ms = fill.params;
|
||||
const auto &config = layer.regions()[fill.region_id]->region().config();
|
||||
const bool external = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface;
|
||||
const bool per_model = external && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model &&
|
||||
(params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral);
|
||||
const bool separate = !external && params.separated_infills &&
|
||||
(is_separable_infill_pattern(params.pattern) || !config.solid_infill_rotate_template.value.empty() ||
|
||||
!config.sparse_infill_rotate_template.value.empty());
|
||||
if (per_model || separate) {
|
||||
double best_overlap = 0.;
|
||||
for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_bboxes.size(); ++i) {
|
||||
const double overlap = area(intersection_ex(layer.lslices[i], expoly));
|
||||
if (overlap > best_overlap) {
|
||||
best_overlap = overlap;
|
||||
const Point center = layer.lslices_separated_component_bboxes[i].center();
|
||||
bbox = layer.object()->bounding_box();
|
||||
bbox.translate(center.x(), center.y());
|
||||
}
|
||||
}
|
||||
}
|
||||
return bbox;
|
||||
}
|
||||
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
void export_group_fills_to_svg(const char *path, const std::vector<SurfaceFill> &fills)
|
||||
{
|
||||
@@ -1353,19 +1380,9 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
||||
|
||||
// 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;
|
||||
params.infill_lock_depth = surface_fill.params.infill_lock_depth;
|
||||
@@ -1389,34 +1406,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
||||
params.can_reverse = false;
|
||||
for (ExPolygon& expoly : surface_fill.expolygons) {
|
||||
|
||||
// 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
|
||||
// Orca: Reuse the body origin used for bridge anchoring, resetting it for each surface.
|
||||
f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox));
|
||||
|
||||
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
|
||||
if (params.symmetric_infill_y_axis) {
|
||||
@@ -1583,8 +1574,14 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
|
||||
params.multiline = surface_fill.params.multiline;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
// Orca: Match make_fills() when choosing the origin of plane-path patterns.
|
||||
// Without the sparse extrusion role, the filler uses each surface's bounds
|
||||
// instead of the object's bounds, so bridge anchors shift away from printed infill.
|
||||
params.extrusion_role = surface_fill.params.extrusion_role;
|
||||
|
||||
for (ExPolygon &expoly : surface_fill.expolygons) {
|
||||
// Orca: Match the per-body origin of make_fills() before generating physical anchors.
|
||||
f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox));
|
||||
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
|
||||
f->spacing = surface_fill.params.spacing;
|
||||
surface_fill.surface.expolygon = std::move(expoly);
|
||||
@@ -1598,6 +1595,25 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
|
||||
return sparse_infill_polylines;
|
||||
}
|
||||
|
||||
// Returns the filament id (1-based) the region is ironed with, or -1 when the
|
||||
// region is not ironed. AllSolid always irons. TopSurfaces and TopmostOnly need
|
||||
// either some top shells or, in spiral mode, more than one bottom shell, and
|
||||
// TopmostOnly additionally needs the layer to be the topmost one.
|
||||
int Layer::choose_ironing_extruder(const PrintRegionConfig &cfg,
|
||||
bool spiral_mode,
|
||||
bool is_topmost_layer)
|
||||
{
|
||||
if (cfg.ironing_type == IroningType::NoIroning)
|
||||
return -1;
|
||||
const bool gate = (cfg.ironing_type == IroningType::AllSolid)
|
||||
|| ((cfg.top_shell_layers > 0 || (spiral_mode && cfg.bottom_shell_layers > 1))
|
||||
&& (cfg.ironing_type == IroningType::TopSurfaces
|
||||
|| (cfg.ironing_type == IroningType::TopmostOnly && is_topmost_layer)));
|
||||
if (!gate)
|
||||
return -1;
|
||||
return cfg.top_surface_filament_id;
|
||||
}
|
||||
|
||||
// Create ironing extrusions over top surfaces.
|
||||
void Layer::make_ironing()
|
||||
{
|
||||
@@ -1667,19 +1683,10 @@ void Layer::make_ironing()
|
||||
if (! layerm->slices.empty()) {
|
||||
IroningParams ironing_params;
|
||||
const PrintRegionConfig &config = layerm->region().config();
|
||||
if (config.ironing_type != IroningType::NoIroning &&
|
||||
(config.ironing_type == IroningType::AllSolid ||
|
||||
((config.top_shell_layers > 0 || (this->object()->print()->config().spiral_mode && config.bottom_shell_layers > 1)) &&
|
||||
(config.ironing_type == IroningType::TopSurfaces ||
|
||||
(config.ironing_type == IroningType::TopmostOnly && layerm->layer()->upper_layer == nullptr))))) {
|
||||
if (config.outer_wall_filament_id == config.top_surface_filament_id || config.wall_loops == 0) {
|
||||
// Iron the whole face.
|
||||
ironing_params.extruder = config.top_surface_filament_id;
|
||||
} else {
|
||||
// Iron just the infill.
|
||||
ironing_params.extruder = config.top_surface_filament_id;
|
||||
}
|
||||
}
|
||||
ironing_params.extruder = Layer::choose_ironing_extruder(
|
||||
config,
|
||||
/*spiral_mode=*/this->object()->print()->config().spiral_mode,
|
||||
/*is_topmost_layer=*/layerm->layer()->upper_layer == nullptr);
|
||||
if (ironing_params.extruder != -1) {
|
||||
//TODO just_infill is currently not used.
|
||||
ironing_params.just_infill = false;
|
||||
|
||||
@@ -14,6 +14,12 @@ namespace Slic3r {
|
||||
|
||||
class ExtrusionEntityCollection;
|
||||
class LayerRegion;
|
||||
class PrintObject;
|
||||
|
||||
// Orca: Share the layer rotation calculation between infill generation and internal
|
||||
// bridge angle selection so both interpret rotation templates in the same way.
|
||||
double calculate_infill_rotation_angle(const PrintObject *object, size_t layer_id,
|
||||
const double &fixed_infill_angle, const std::string &template_string);
|
||||
|
||||
// An interface class to Perl, aggregating an instance of a Fill and a FillData.
|
||||
class Filler
|
||||
|
||||
@@ -1395,8 +1395,8 @@ void Filler::_fill_surface_single(
|
||||
}
|
||||
#endif /* ADAPTIVE_CUBIC_INFILL_DEBUG_OUTPUT */
|
||||
|
||||
const auto hook_length = coordf_t(std::min<float>(std::numeric_limits<coord_t>::max(), scale_(params.anchor_length)));
|
||||
const auto hook_length_max = coordf_t(std::min<float>(std::numeric_limits<coord_t>::max(), scale_(params.anchor_length_max)));
|
||||
const auto hook_length = coordf_t(scale_(params.anchor_length));
|
||||
const auto hook_length_max = coordf_t(scale_(params.anchor_length_max));
|
||||
|
||||
Polylines all_polylines_with_hooks = all_polylines.size() > 1 ? connect_lines_using_hooks(std::move(all_polylines), expolygon, this->spacing, hook_length, hook_length_max) : std::move(all_polylines);
|
||||
|
||||
|
||||
@@ -3090,10 +3090,11 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
case 0: // Grid / Trapezoidal
|
||||
{
|
||||
// Generate a non-crossing trapezoidal pattern to avoid overextrusion at intersections when `multiline > 1`.
|
||||
// P2--P3
|
||||
// / \
|
||||
// P0_P1/ \P4_
|
||||
//
|
||||
/*
|
||||
* P2--P3
|
||||
* / \
|
||||
* P0_P1/ \P4_
|
||||
*/
|
||||
// P0xP1x=P4xP0x=d1/2
|
||||
// P2xP3x=d1
|
||||
// P1yP2y=P2yP3y=d2
|
||||
@@ -3171,10 +3172,12 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
case 1: // Triangular
|
||||
{
|
||||
// Generate a non-crossing trapezoidal pattern with a base line below.
|
||||
// P1-P2
|
||||
// / \
|
||||
// P0/ \P3_P4
|
||||
// ----------------
|
||||
/*
|
||||
* P1-P2
|
||||
* / \
|
||||
* P0/ \P3_P4
|
||||
* ----------------
|
||||
*/
|
||||
// P1xP2x=P3xP4x=d2
|
||||
// P0yP1y=P2yP3y=h-2d1
|
||||
//
|
||||
|
||||
@@ -40,8 +40,8 @@ static float DeltaHS_BBS(float h1, float s1, float v1, float h2, float s2, float
|
||||
return std::min(1.2f, dxy);
|
||||
}
|
||||
|
||||
FlushVolCalculator::FlushVolCalculator(int min, int max, int flush_dataset, float multiplier)
|
||||
:m_min_flush_vol(min), m_max_flush_vol(max), m_multiplier(multiplier), m_flush_dataset(flush_dataset)
|
||||
FlushVolCalculator::FlushVolCalculator(int min, int max, int flush_dataset)
|
||||
:m_min_flush_vol(min), m_max_flush_vol(max), m_flush_dataset(flush_dataset)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ extern const int g_max_flush_volume;
|
||||
class FlushVolCalculator
|
||||
{
|
||||
public:
|
||||
FlushVolCalculator(int min, int max, int flush_dataset, float multiplier = 1.0f);
|
||||
FlushVolCalculator(int min, int max, int flush_dataset);
|
||||
~FlushVolCalculator()
|
||||
{
|
||||
}
|
||||
@@ -32,7 +32,6 @@ public:
|
||||
private:
|
||||
int m_min_flush_vol;
|
||||
int m_max_flush_vol;
|
||||
float m_multiplier;
|
||||
int m_flush_dataset;
|
||||
};
|
||||
|
||||
|
||||
@@ -102,45 +102,6 @@ struct ZipUnicodePathExtraField
|
||||
}
|
||||
};
|
||||
|
||||
// Validate that a relative file path does not escape the root directory via path traversal.
|
||||
static bool is_path_within_root(const std::string& file_path, const boost::filesystem::path& root)
|
||||
{
|
||||
if (file_path.empty())
|
||||
return false;
|
||||
|
||||
boost::filesystem::path p(file_path);
|
||||
if (p.is_absolute())
|
||||
return false;
|
||||
|
||||
// Reject any path component that is ".."
|
||||
for (const auto& component : p) {
|
||||
if (component == "..")
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve the full path and verify it starts with the canonical root (also catches symlink escapes)
|
||||
try {
|
||||
boost::filesystem::path full_path = root / p;
|
||||
boost::filesystem::path canonical_root = boost::filesystem::weakly_canonical(root);
|
||||
boost::filesystem::path canonical_full = boost::filesystem::weakly_canonical(full_path);
|
||||
|
||||
auto root_str = canonical_root.string();
|
||||
auto full_str = canonical_full.string();
|
||||
if (full_str.length() < root_str.length())
|
||||
return false;
|
||||
if (full_str.compare(0, root_str.length(), root_str) != 0)
|
||||
return false;
|
||||
// Ensure it's a proper prefix (not just a substring of a longer directory name)
|
||||
if (full_str.length() > root_str.length() &&
|
||||
full_str[root_str.length()] != boost::filesystem::path::preferred_separator)
|
||||
return false;
|
||||
} catch (const boost::filesystem::filesystem_error&) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// VERSION NUMBERS
|
||||
// 0 : .3mf, files saved by older slic3r or other applications. No version definition in them.
|
||||
// 1 : Introduction of 3mf versioning. No other change in data saved into 3mf files.
|
||||
@@ -685,6 +646,11 @@ bool bbs_is_valid_object_type(const std::string& type)
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool is_published_3mf_flag(const std::string &value)
|
||||
{
|
||||
return value == "1";
|
||||
}
|
||||
|
||||
void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
{
|
||||
if (!result) return;
|
||||
@@ -1221,6 +1187,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
// add backup & restore logic
|
||||
bool _load_model_from_file(std::string filename, Model& model, PlateDataPtrs& plate_data_list, std::vector<Preset*>& project_presets, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Import3mfProgressFn proFn = nullptr,
|
||||
BBLProject* project = nullptr, int plate_id = 0);
|
||||
|
||||
// A minimal published 3MF carries no slicer tags (any tag would make old receivers show
|
||||
// a baked-in, wrong "old version" popup on their geometry-only fallback), so it
|
||||
// classifies as From_Other. It is still a fully structured OrcaSlicer file though:
|
||||
// identified by its own metadata, it keeps BBS-grade geometry handling (no instance
|
||||
// splitting, no transform baking, no renaming) in this build. Old receivers without the
|
||||
// publish feature don't know the metadata and take their third-party geometry path.
|
||||
// Reads the parse-time metadata: the model XML carries it before its resources, while
|
||||
// m_model->model_info is only filled in after the whole XML has been parsed.
|
||||
bool _is_published_3mf() const {
|
||||
const auto it = this->model_info.metadata_items.find(ORCA_PUBLISHED_TAG);
|
||||
return it != this->model_info.metadata_items.end() && is_published_3mf_flag(it->second);
|
||||
}
|
||||
|
||||
bool _is_svg_shape_file(const std::string &filename) const;
|
||||
bool _extract_from_archive(mz_zip_archive& archive, std::string const & path, std::function<bool (mz_zip_archive& archive, const mz_zip_archive_file_stat& stat)>, bool restore = false);
|
||||
bool _extract_xml_from_archive(mz_zip_archive& archive, std::string const & path, XML_StartElementHandler start_handler, XML_EndElementHandler end_handler);
|
||||
@@ -2054,7 +2034,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
lock.close();
|
||||
|
||||
if (!m_is_bbl_3mf) {
|
||||
if (!m_is_bbl_3mf && !_is_published_3mf()) {
|
||||
// if the 3mf was not produced by OrcaSlicer and there is more than one instance,
|
||||
// split the object in as many objects as instances
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", found 3mf from other vendor, split as instance");
|
||||
@@ -3618,7 +3598,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
m_index_paths.insert({ object.first.second, object.first.first});
|
||||
}
|
||||
|
||||
if (!m_is_bbl_3mf) {
|
||||
if (!m_is_bbl_3mf && !_is_published_3mf()) {
|
||||
// if the 3mf was not produced by OrcaSlicer and there is only one object,
|
||||
// set the object name to match the filename
|
||||
if (m_model->objects.size() == 1)
|
||||
@@ -5341,7 +5321,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
TriangleMesh triangle_mesh(std::move(its), volume_data.mesh_stats);
|
||||
|
||||
if (!m_is_bbl_3mf) {
|
||||
if (!m_is_bbl_3mf && !_is_published_3mf()) {
|
||||
// if the 3mf was not produced by OrcaSlicer and there is only one instance,
|
||||
// bake the transformation into the geometry to allow the reload from disk command
|
||||
// to work properly
|
||||
@@ -5987,6 +5967,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
bool m_save_gcode { false }; // whether to save gcode for normal save
|
||||
bool m_skip_model { false }; // skip model when exporting .gcode.3mf
|
||||
bool m_skip_auxiliary { false }; // skip normal axuiliary files
|
||||
bool m_minimal_published { false }; // published 3MF: omit the project config, the embedded preset files and the slicer tags
|
||||
bool m_use_loaded_id { false }; // whether to use loaded id for identify_id
|
||||
bool m_share_mesh { false }; // whether to share mesh between objects
|
||||
std::string m_thumbnail_middle = PRINTER_THUMBNAIL_MIDDLE_FILE;
|
||||
@@ -6087,6 +6068,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
m_skip_auxiliary = store_params.strategy & SaveStrategy::SkipAuxiliary;
|
||||
m_share_mesh = store_params.strategy & SaveStrategy::ShareMesh;
|
||||
m_from_backup_save = store_params.strategy & SaveStrategy::Backup;
|
||||
m_minimal_published = store_params.strategy & SaveStrategy::MinimalPublished;
|
||||
|
||||
m_use_loaded_id = store_params.strategy & SaveStrategy::UseLoadedId;
|
||||
|
||||
@@ -6501,7 +6483,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
// Adds slic3r print config file ("Metadata/Slic3r_PE.config").
|
||||
// This file contains the content of FullPrintConfig / SLAFullPrintConfig.
|
||||
if (config != nullptr) {
|
||||
// Omitted for minimal published 3MF: OrcaSlicer versions without the publish feature
|
||||
// then fall back to importing the geometry only, and new versions read the published
|
||||
// payload from the model metadata instead.
|
||||
if (config != nullptr && !m_minimal_published) {
|
||||
// BBS: change to json format
|
||||
// if (!_add_print_config_file_to_archive(archive, *config)) {
|
||||
if (!_add_project_config_file_to_archive(archive, *config, model)) { return false; }
|
||||
@@ -6514,8 +6499,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (cb_cancel) return false;
|
||||
}
|
||||
|
||||
// BBS: add project config
|
||||
if (project_presets.size() > 0) {
|
||||
// BBS: add project config (omitted for minimal published 3MF)
|
||||
if (!m_minimal_published && project_presets.size() > 0) {
|
||||
// BBS: add project embedded preset files
|
||||
_add_project_embedded_presets_to_archive(archive, model, project_presets);
|
||||
|
||||
@@ -6987,10 +6972,31 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
// Orca: PRIVACY: do not store creation & modification date in 3mf
|
||||
metadata_item_map[BBL_CREATION_DATE_TAG] = "";
|
||||
metadata_item_map[BBL_MODIFICATION_TAG] = "";
|
||||
// Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION
|
||||
metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str();
|
||||
// Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION.
|
||||
// A minimal published 3MF writes no slicer tags at all: any tag would route old
|
||||
// receivers onto a geometry-only fallback whose baked-in popup misreports the
|
||||
// file ("old OrcaSlicer version" / "BambuStudio"), while tag-less files classify
|
||||
// as From_Other and import the geometry silently.
|
||||
if (m_minimal_published) {
|
||||
// metadata_item_map is seeded from the input file's metadata_items above, so a
|
||||
// project opened from a regular Orca/BBS 3MF still carries the slicer-identifying
|
||||
// tags it came with. Erase every one of them - not just the two most common -
|
||||
// so a published 3MF is fully tag-less: old receivers classify it as From_Other
|
||||
// and import the geometry silently instead of showing a baked-in "old version"
|
||||
// popup, and no version marker survives to seed a later re-save.
|
||||
metadata_item_map.erase(BBL_APPLICATION_TAG);
|
||||
metadata_item_map.erase(ORCASLICER_TAG);
|
||||
metadata_item_map.erase(BBS_3MF_VERSION);
|
||||
metadata_item_map.erase(BBS_3MF_VERSION1);
|
||||
} else {
|
||||
metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str();
|
||||
}
|
||||
}
|
||||
metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF);
|
||||
// The Bambu 3MF version marker is part of the slicer identity: omit it for a minimal
|
||||
// published file along with the tags erased above (skipping the overwrite alone would
|
||||
// leave the value the source file seeded into metadata_item_map).
|
||||
if (!m_minimal_published)
|
||||
metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF);
|
||||
|
||||
if (!model.mk_name.empty()) {
|
||||
metadata_item_map[BBL_MAKERLAB_TAG] = xml_escape(model.mk_name);
|
||||
@@ -7013,7 +7019,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
BOOST_LOG_TRIVIAL(info) << "bbs_3mf: save key= " << item.first << ", value = " << item.second;
|
||||
stream << " <" << METADATA_TAG << " name=\"" << item.first << "\">"
|
||||
<< xml_escape(item.second) << "</" << METADATA_TAG << ">\n";
|
||||
if (item.first == BBL_APPLICATION_TAG) {
|
||||
if (item.first == BBL_APPLICATION_TAG && !m_minimal_published) {
|
||||
// The OrcaSlicer tag is only written for files that carry the Application
|
||||
// tag, which a minimal published 3MF erases (see the map assignment above):
|
||||
// the explicit !m_minimal_published guard keeps the tag-less guarantee from
|
||||
// depending on that erase happening to run first.
|
||||
stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">"
|
||||
<< xml_escape(SoftFever_VERSION) << "</" << METADATA_TAG << ">\n";
|
||||
}
|
||||
@@ -8866,7 +8876,7 @@ public:
|
||||
auto model = object.get_model();
|
||||
auto o = m_temp_model.add_object(object);
|
||||
int backup_id = model->get_object_backup_id(object);
|
||||
push_task({ AddObject, (size_t) backup_id, object.get_model()->get_backup_path(), o, 1 });
|
||||
push_task({ AddObject, (size_t) backup_id, object.get_model()->get_backup_path(), o, { 1 } });
|
||||
}
|
||||
|
||||
void remove_object_mesh(ModelObject& object) {
|
||||
@@ -8876,7 +8886,7 @@ public:
|
||||
void backup_soon() {
|
||||
boost::lock_guard lock(m_mutex);
|
||||
m_other_changes_backup = true;
|
||||
m_tasks.push_back({ Backup, 0, std::string(), nullptr, ++m_task_seq });
|
||||
m_tasks.push_back({ Backup, 0, std::string(), nullptr, { ++m_task_seq } });
|
||||
m_cond.notify_all();
|
||||
}
|
||||
|
||||
@@ -8894,7 +8904,7 @@ public:
|
||||
m_ui_tasks.clear();
|
||||
m_tasks.clear();
|
||||
}
|
||||
m_tasks.push_back({ RemoveBackup, model.id().id, model.get_backup_path(), nullptr, removeAll });
|
||||
m_tasks.push_back({ RemoveBackup, model.id().id, model.get_backup_path(), nullptr, { removeAll } });
|
||||
++m_task_seq;
|
||||
if (model.is_need_backup()) {
|
||||
m_other_changes = false;
|
||||
@@ -9109,7 +9119,7 @@ public:
|
||||
else
|
||||
m_cond.wait(lock);
|
||||
if (m_interval > 0 && boost::get_system_time() > m_next_backup) {
|
||||
m_tasks.push_back({ Backup, 0, std::string(), nullptr, ++m_task_seq });
|
||||
m_tasks.push_back({ Backup, 0, std::string(), nullptr, { ++m_task_seq } });
|
||||
m_next_backup += boost::posix_time::seconds(m_interval);
|
||||
// Maybe wakeup from power sleep
|
||||
if (m_next_backup < boost::get_system_time())
|
||||
@@ -9205,6 +9215,126 @@ std::string bbs_3mf_get_thumbnail(const char *path)
|
||||
return data;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Parses just the model-file <metadata> elements, mirroring the importer's
|
||||
// _handle_start_metadata/_handle_end_metadata (attribute-order independent, entity-unescaped,
|
||||
// whitespace tolerant). Stops the parser as soon as the published flag node is read so the
|
||||
// geometry/resources that follow are skipped, which keeps the per-file cost small.
|
||||
struct PublishedXmlProbe
|
||||
{
|
||||
XML_Parser parser{nullptr};
|
||||
bool in_metadata{false};
|
||||
bool found{false};
|
||||
bool published{false};
|
||||
std::string curr_name;
|
||||
std::string curr_value;
|
||||
|
||||
static std::string attribute(const char** attrs, const char* key)
|
||||
{
|
||||
if (attrs == nullptr)
|
||||
return std::string();
|
||||
// expat hands the attrs as a NULL-terminated {name, value, ...} array.
|
||||
for (unsigned int a = 0; attrs[a] != nullptr; a += 2)
|
||||
if (::strcmp(attrs[a], key) == 0 && attrs[a + 1] != nullptr)
|
||||
return attrs[a + 1];
|
||||
return std::string();
|
||||
}
|
||||
|
||||
static void XMLCALL start(void* user_data, const char* name, const char** attrs)
|
||||
{
|
||||
auto* self = static_cast<PublishedXmlProbe*>(user_data);
|
||||
if (::strcmp(name, METADATA_TAG) == 0) {
|
||||
self->in_metadata = true;
|
||||
self->curr_name = attribute(attrs, NAME_ATTR);
|
||||
self->curr_value.clear();
|
||||
} else {
|
||||
self->in_metadata = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void XMLCALL characters(void* user_data, const XML_Char* s, int len)
|
||||
{
|
||||
auto* self = static_cast<PublishedXmlProbe*>(user_data);
|
||||
if (self->in_metadata)
|
||||
self->curr_value.append(s, len);
|
||||
}
|
||||
|
||||
static void XMLCALL end(void* user_data, const char* name)
|
||||
{
|
||||
auto* self = static_cast<PublishedXmlProbe*>(user_data);
|
||||
if (!self->in_metadata || ::strcmp(name, METADATA_TAG) != 0)
|
||||
return;
|
||||
self->in_metadata = false;
|
||||
if (self->curr_name == ORCA_PUBLISHED_TAG) {
|
||||
self->published = is_published_3mf_flag(xml_unescape(self->curr_value));
|
||||
self->found = true;
|
||||
if (self->parser != nullptr)
|
||||
XML_StopParser(self->parser, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool bbs_3mf_is_published(const std::string &path)
|
||||
{
|
||||
mz_zip_archive archive;
|
||||
mz_zip_zero_struct(&archive);
|
||||
|
||||
struct close_lock
|
||||
{
|
||||
mz_zip_archive *archive;
|
||||
void close()
|
||||
{
|
||||
if (archive) {
|
||||
close_zip_reader(archive);
|
||||
archive = nullptr;
|
||||
}
|
||||
}
|
||||
~close_lock() { close(); }
|
||||
} lock{&archive};
|
||||
|
||||
if (!open_zip_reader(&archive, path))
|
||||
return false;
|
||||
|
||||
// Read just the model XML (the metadata node sits before the resources, so the probe below
|
||||
// stops early) rather than by a raw substring match; no geometry parsing.
|
||||
int index = mz_zip_reader_locate_file(&archive, MODEL_FILE.c_str(), nullptr, 0);
|
||||
if (index < 0)
|
||||
return false;
|
||||
mz_zip_archive_file_stat stat;
|
||||
if (!mz_zip_reader_file_stat(&archive, index, &stat))
|
||||
return false;
|
||||
std::string xml(stat.m_uncomp_size, '\0');
|
||||
if (!mz_zip_reader_extract_to_mem(&archive, index, xml.data(), xml.size(), 0))
|
||||
return false;
|
||||
|
||||
XML_Parser parser = XML_ParserCreate(nullptr);
|
||||
if (parser == nullptr)
|
||||
return false;
|
||||
|
||||
PublishedXmlProbe probe;
|
||||
probe.parser = parser;
|
||||
XML_SetUserData(parser, &probe);
|
||||
XML_SetElementHandler(parser, PublishedXmlProbe::start, PublishedXmlProbe::end);
|
||||
XML_SetCharacterDataHandler(parser, PublishedXmlProbe::characters);
|
||||
// Never resolve external entities from a file we are only probing.
|
||||
XML_SetExternalEntityRefHandler(parser, nullptr);
|
||||
XML_SetEntityDeclHandler(parser, nullptr);
|
||||
|
||||
const XML_Status status = XML_Parse(parser, xml.data(), static_cast<int>(xml.size()), 1);
|
||||
// XML_StopParser(parser, false) from the end handler makes XML_Parse return
|
||||
// XML_STATUS_ERROR with XML_ERROR_ABORTED - treat that as success (we stopped on the flag).
|
||||
const bool parse_ok = (status == XML_STATUS_OK) ||
|
||||
(XML_GetErrorCode(parser) == XML_ERROR_ABORTED && probe.found);
|
||||
XML_ParserFree(parser);
|
||||
|
||||
if (!parse_ok)
|
||||
return false;
|
||||
return probe.published;
|
||||
}
|
||||
|
||||
bool load_gcode_3mf_from_stream(std::istream &data, DynamicPrintConfig *config, Model *model, PlateDataPtrs *plate_data_list, Semver *file_version)
|
||||
{
|
||||
CNumericLocalesSetter locales_setter;
|
||||
|
||||
@@ -159,12 +159,28 @@ enum class SaveStrategy
|
||||
SkipAuxiliary = 1 << 9,
|
||||
UseLoadedId = 1 << 10,
|
||||
ShareMesh = 1 << 11,
|
||||
// Keep this separate from SplitModel, which uses the 0x1000 bit as part of its
|
||||
// production-extension value.
|
||||
MinimalPublished = 1 << 13,
|
||||
|
||||
SplitModel = 0x1000 | ProductionExt,
|
||||
Encrypted = SecureContentExt | SplitModel,
|
||||
Backup = 0x10000 | WithGcode | Silence | SkipStatic | SplitModel,
|
||||
};
|
||||
|
||||
// Model metadata keys of a "published" 3MF (see MinimalPublished): the flag marks a minimal,
|
||||
// tag-less publish export, the others carry the author-selected settings payload. Namespaced
|
||||
// with the "orca_published" prefix because metadata_items round-trips verbatim through other
|
||||
// slicers, where a bare "published" key could collide.
|
||||
inline constexpr const char *ORCA_PUBLISHED_TAG = "orca_published";
|
||||
inline constexpr const char *ORCA_PUBLISHED_KEYS_TAG = "orca_published_keys";
|
||||
inline constexpr const char *ORCA_PUBLISHED_MATERIAL_TAG = "orca_published_material_keys";
|
||||
inline constexpr const char *ORCA_PUBLISHED_CONFIG_TAG = "orca_published_config";
|
||||
|
||||
// Published files are produced with "1". The importer and the GUI loader both gate on this
|
||||
// exact value, so a "0"/"false"/unknown value is rejected consistently.
|
||||
bool is_published_3mf_flag(const std::string &value);
|
||||
|
||||
inline SaveStrategy operator | (SaveStrategy lhs, SaveStrategy rhs)
|
||||
{
|
||||
using T = std::underlying_type_t <SaveStrategy>;
|
||||
@@ -277,6 +293,9 @@ extern bool load_bbs_3mf(const char* path, DynamicPrintConfig* config, ConfigSub
|
||||
|
||||
extern std::string bbs_3mf_get_thumbnail(const char * path);
|
||||
|
||||
// Lightweight check: does this 3mf carry the "published" (orca_published == "1") marker? Only reads the 3D/3dmodel.model metadata node
|
||||
extern bool bbs_3mf_is_published(const std::string &path);
|
||||
|
||||
extern bool load_gcode_3mf_from_stream(std::istream & data, DynamicPrintConfig* config, Model* model, PlateDataPtrs* plate_data_list,
|
||||
Semver* file_version);
|
||||
|
||||
|
||||
+194
-104
@@ -1,5 +1,6 @@
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "GCode/WipePathHelpers.hpp"
|
||||
#include "GCodeWriter.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
@@ -438,7 +439,6 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
auto& writer = gcodegen.writer();
|
||||
auto& config = gcodegen.config();
|
||||
auto extruder = writer.filament();
|
||||
auto extruder_id = extruder->extruder_id();
|
||||
auto last_pos = gcodegen.last_pos();
|
||||
|
||||
// Declare & initialize retraction lengths
|
||||
@@ -475,13 +475,13 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
wipe_speed = std::max(wipe_speed, 10.0);
|
||||
|
||||
// Process wipe path & calculate wipe path length
|
||||
double wipe_dist = scale_(config.wipe_distance.get_at(extruder_id));
|
||||
double wipe_dist = scale_(config.wipe_distance.get_at(extruder->config_index()));
|
||||
Polyline wipe_path = {last_pos};
|
||||
wipe_path.append(this->path.points.begin() + 1, this->path.points.end());
|
||||
double wipe_path_length = std::min(wipe_path.length(), wipe_dist);
|
||||
|
||||
// Calculate the maximum retraction amount during wipe
|
||||
retraction_length_during_wipe = config.retraction_speed.get_at(extruder_id) *
|
||||
retraction_length_during_wipe = config.retraction_speed.get_at(extruder->config_index()) *
|
||||
unscale_(wipe_path_length) / wipe_speed;
|
||||
|
||||
// If the maximum retraction amount during wipe is too small,
|
||||
@@ -564,6 +564,16 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
return default_value;
|
||||
}
|
||||
|
||||
// Orca: rebuild the stored wipe path while preserving Polyline's boundary deduplication.
|
||||
void Wipe::update_path(const ExtrusionPaths &paths, bool reverse)
|
||||
{
|
||||
reset_path();
|
||||
for (const ExtrusionPath& extrusion_path : paths)
|
||||
path.append(extrusion_path.polyline.to_polyline());
|
||||
if (reverse)
|
||||
path.reverse();
|
||||
}
|
||||
|
||||
std::string Wipe::wipe(GCode& gcodegen,double length, bool toolchange, bool is_last)
|
||||
{
|
||||
std::string gcode;
|
||||
@@ -616,14 +626,11 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
if (gcodegen.enable_cooling_markers() && !is_last)
|
||||
cooling_mark = /*gcodegen.config().role_based_wipe_speed ? ";_EXTERNAL_PERIMETER" : */";_WIPE";
|
||||
|
||||
// Orca: set speed once because wipe_speed is constant for all segments.
|
||||
gcode += gcodegen.writer().set_speed(_wipe_speed * 60, "", cooling_mark);
|
||||
for (const Line& line : wipe_path.lines()) {
|
||||
double segment_length = line.length();
|
||||
double dE = length * (segment_length / wipe_dist);
|
||||
//BBS: fix this FIXME
|
||||
//FIXME one shall not generate the unnecessary G1 Fxxx commands, here wipe_speed is a constant inside this cycle.
|
||||
// Is it here for the cooling markers? Or should it be outside of the cycle?
|
||||
//gcode += gcodegen.writer().set_speed(wipe_speed * 60, "", gcodegen.enable_cooling_markers() ? ";_WIPE" : "");
|
||||
gcode += gcodegen.writer().extrude_to_xy(
|
||||
gcodegen.point_to_gcode(line.b),
|
||||
-dE,
|
||||
@@ -1021,11 +1028,21 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
double current_z = gcodegen.writer().get_position().z();
|
||||
if (z == -1.) // in case no specific z was provided, print at current_z pos
|
||||
z = current_z;
|
||||
if (!is_approx(z, current_z)) {
|
||||
// Orca: wipe_tower_no_sparse_layers crash guard. With sparse layers skipped the tower is
|
||||
// compacted far below the object, so descending to it is only safe once the nozzle is parked
|
||||
// over the tower - which is what the is_finish_first travel above does. Otherwise the nozzle
|
||||
// is still over the model and this descent would drive it into the print, so defer it to the
|
||||
// re-descents below, which run after the travel to the tower.
|
||||
const bool defer_compacted_descend = m_sparse_layers_skipped
|
||||
&& !tcr.priming && !tcr.is_finish_first && (current_z - z) > EPSILON;
|
||||
if (!is_approx(z, current_z) && !defer_compacted_descend) {
|
||||
gcode += gcodegen.writer().retract();
|
||||
gcode += gcodegen.writer().travel_to_z(z, "Travel down to the last wipe tower layer.");
|
||||
gcode += gcodegen.writer().unretract();
|
||||
}
|
||||
// Tower compacted below the object, so any extrusion emitted without an explicit z has to be
|
||||
// pulled back down to it first.
|
||||
const bool compacted_below_object = m_sparse_layers_skipped && z >= 0. && (tcr.print_z - z) > EPSILON;
|
||||
|
||||
// Process the end filament gcode.
|
||||
bool add_change_filament_624 = false;
|
||||
@@ -1078,11 +1095,23 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
std::string nozzle_change_gcode_trans;
|
||||
if (is_nozzle_change) {
|
||||
// move to start_pos before nozzle change
|
||||
// Orca: travel_to() lifts to the object layer height to clear the print. That lift is
|
||||
// needed when arriving from the model, but is a wasted full-height Z bounce when the
|
||||
// nozzle already sits on the compacted tower, so travel at the compacted z instead.
|
||||
const bool compact_intower_nc_travel = compacted_below_object
|
||||
&& (tcr.print_z - gcodegen.writer().get_position().z()) > EPSILON;
|
||||
std::string start_pos_str;
|
||||
start_pos_str = gcodegen.travel_to(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(tcr.nozzle_change_result.start_pos) + plate_origin_2d), erMixed,
|
||||
"Move to nozzle change start pos");
|
||||
"Move to nozzle change start pos", compact_intower_nc_travel ? z : DBL_MAX);
|
||||
check_add_eol(start_pos_str);
|
||||
nozzle_change_gcode_trans += start_pos_str;
|
||||
// The nozzle-change wipe below carries no explicit z, so it would extrude at the object
|
||||
// layer height and float above the compacted tower. Descend unless the travel stayed down.
|
||||
if (!compact_intower_nc_travel && compacted_below_object) {
|
||||
std::string nc_z_descend = gcodegen.writer().travel_to_z(z, "Descend to compacted wipe tower z (no sparse layers)");
|
||||
check_add_eol(nc_z_descend);
|
||||
nozzle_change_gcode_trans += nc_z_descend;
|
||||
}
|
||||
nozzle_change_gcode_trans += gcodegen.unretract();
|
||||
nozzle_change_gcode_trans += transform_gcode(tcr.nozzle_change_result.gcode, tcr.nozzle_change_result.start_pos, wipe_tower_offset, wipe_tower_rotation);
|
||||
gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(tcr.nozzle_change_result.end_pos) + plate_origin_2d));
|
||||
@@ -1421,6 +1450,15 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
|
||||
start_filament_gcode_str = start_filament_gcode_str + wipe_next_start_point_str + toolchange_unretract_str;
|
||||
|
||||
// Orca: the custom change_filament_gcode lifts to the object layer height and the unretract
|
||||
// de-hops back to it, so every tower extrusion emitted after it (purge moves, and the wall
|
||||
// when it prints after the toolchange) would float above the compacted tower. Descend first.
|
||||
if (compacted_below_object) {
|
||||
std::string z_descend = gcodegen.writer().travel_to_z(z, "Descend to compacted wipe tower z (no sparse layers)");
|
||||
check_add_eol(z_descend);
|
||||
start_filament_gcode_str += z_descend;
|
||||
}
|
||||
|
||||
// Insert the end filament, toolchange, and start filament gcode into the generated gcode.
|
||||
DynamicConfig config;
|
||||
config.set_key_value("filament_end_gcode", new ConfigOptionString(end_filament_gcode_str));
|
||||
@@ -1908,11 +1946,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
// resulting in a wipe tower with sparse layers.
|
||||
double wipe_tower_z = -1;
|
||||
bool ignore_sparse = false;
|
||||
if (gcodegen.config().wipe_tower_no_sparse_layers.value) {
|
||||
if (m_sparse_layers_skipped) {
|
||||
wipe_tower_z = m_last_wipe_tower_print_z;
|
||||
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 &&
|
||||
m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool &&
|
||||
m_layer_idx != 0);
|
||||
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]) && m_layer_idx != 0;
|
||||
if (m_tool_change_idx == 0 && !ignore_sparse)
|
||||
wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height;
|
||||
}
|
||||
@@ -1928,12 +1964,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
// resulting in a wipe tower with sparse layers.
|
||||
double wipe_tower_z = -1;
|
||||
bool ignore_sparse = false;
|
||||
if (gcodegen.config().wipe_tower_no_sparse_layers.value) {
|
||||
wipe_tower_z = m_last_wipe_tower_print_z;
|
||||
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 &&
|
||||
m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool);
|
||||
if (m_tool_change_idx == 0 && !ignore_sparse)
|
||||
wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height;
|
||||
if (m_sparse_layers_skipped) {
|
||||
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
|
||||
wipe_tower_z = m_compacted_tower_z[m_layer_idx];
|
||||
}
|
||||
|
||||
if ((m_enable_timelapse_print || m_enable_wrapping_detection) && m_is_first_print) {
|
||||
@@ -1946,10 +1979,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size()))
|
||||
throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer.");
|
||||
|
||||
if (!ignore_sparse) {
|
||||
if (!ignore_sparse)
|
||||
gcode += append_tcr(gcodegen, m_tool_changes[m_layer_idx][m_tool_change_idx++], extruder_id, wipe_tower_z);
|
||||
m_last_wipe_tower_print_z = wipe_tower_z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1963,9 +1994,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
return true;
|
||||
|
||||
bool ignore_sparse = false;
|
||||
if (gcodegen.config().wipe_tower_no_sparse_layers.value) {
|
||||
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 && m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool);
|
||||
}
|
||||
if (m_sparse_layers_skipped)
|
||||
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
|
||||
|
||||
if ((m_enable_timelapse_print || m_enable_wrapping_detection) && m_is_first_print) {
|
||||
return false;
|
||||
@@ -2901,6 +2931,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
const bool skip_config_block = print.config().gcode_skip_config_block;
|
||||
const WipeTowerType wipe_tower_type = print.wipe_tower_type();
|
||||
m_calib_config.clear();
|
||||
// Orca: Calibration overrides are reapplied after object/region settings in _extrude().
|
||||
// Keep inward wiping from masking retraction and pressure advance artifacts.
|
||||
switch (print.calib_mode()) {
|
||||
case CalibMode::Calib_PA_Line:
|
||||
case CalibMode::Calib_PA_Pattern:
|
||||
case CalibMode::Calib_PA_Tower:
|
||||
case CalibMode::Calib_Auto_PA_Line:
|
||||
case CalibMode::Calib_Retraction_tower:
|
||||
m_calib_config.set_key_value("wipe_inward", new ConfigOptionBool(false));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// resets analyzer's tracking data
|
||||
m_last_height = 0.f;
|
||||
m_last_layer_z = 0.f;
|
||||
@@ -3555,7 +3598,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
|
||||
auto used_filaments = print.get_slice_used_filaments(false);
|
||||
this->placeholder_parser().set("is_all_bbl_filament", std::all_of(used_filaments.begin(), used_filaments.end(), [&](auto idx) {
|
||||
return m_config.filament_vendor.values[idx] == "Bambu Lab";
|
||||
return m_config.filament_vendor.get_at(idx) == "Bambu Lab";
|
||||
}));
|
||||
|
||||
//add during_print_exhaust_fan_speed
|
||||
@@ -3572,7 +3615,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
this->placeholder_parser().set("outer_wall_volumetric_speed", new ConfigOptionFloat(outer_wall_volumetric_speed));
|
||||
|
||||
auto first_layer_filaments = print.get_slice_used_filaments(true);
|
||||
bool has_tpu_in_first_layer = std::any_of(first_layer_filaments.begin(), first_layer_filaments.end(), [&](unsigned int idx) { return m_config.filament_type.values[idx] == "TPU"; });
|
||||
bool has_tpu_in_first_layer = std::any_of(first_layer_filaments.begin(), first_layer_filaments.end(), [&](unsigned int idx) { return m_config.filament_type.get_at(idx) == "TPU"; });
|
||||
this->placeholder_parser().set("has_tpu_in_first_layer", new ConfigOptionBool(has_tpu_in_first_layer));
|
||||
|
||||
if (print.calib_params().mode == CalibMode::Calib_PA_Line) {
|
||||
@@ -6308,8 +6351,13 @@ LayerResult GCode::process_layer(
|
||||
all_label_ids.insert(inst.label_object_id);
|
||||
break;
|
||||
}
|
||||
std::vector<size_t> filament_instances_id(all_label_ids.begin(), all_label_ids.end());
|
||||
m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id);
|
||||
// Orca: A scheduled extruder may have no object instances on this layer.
|
||||
// Clear any pending mask so it cannot be emitted for the wrong toolchange.
|
||||
m_filament_instances_code.clear();
|
||||
if (!all_label_ids.empty()) {
|
||||
std::vector<size_t> filament_instances_id(all_label_ids.begin(), all_label_ids.end());
|
||||
m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id);
|
||||
}
|
||||
}
|
||||
|
||||
// The inline _extrude hook may already have taken the snapshot mid-extrusion on a
|
||||
@@ -6555,6 +6603,8 @@ LayerResult GCode::process_layer(
|
||||
}
|
||||
// Then print infill
|
||||
gcode += this->extrude_infill(print, by_region_specific, false);
|
||||
// Then the walls left hanging in mid air, now that the infill can anchor them
|
||||
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false, true);
|
||||
// Then print perimeters of regions that has is_infill_first == true
|
||||
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
|
||||
}
|
||||
@@ -6850,6 +6900,7 @@ LayerResult GCode::process_layer(
|
||||
has_insert_timelapse_gcode = true;
|
||||
}
|
||||
gcode += this->extrude_infill(print, by_region_specific, false);
|
||||
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false, true);
|
||||
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
|
||||
// ironing
|
||||
gcode += this->extrude_infill(print, by_region_specific, true);
|
||||
@@ -7199,7 +7250,8 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
|
||||
const std::string& description,
|
||||
double speed,
|
||||
const ExtrusionEntitiesPtr& region_perimeters,
|
||||
const Point* start_point)
|
||||
const Point* start_point,
|
||||
const WipeInwardSupport* wipe_support)
|
||||
{
|
||||
// get a copy; don't modify the orientation of the original loop object otherwise
|
||||
// next copies (if any) would not detect the correct orientation
|
||||
@@ -7429,63 +7481,80 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
|
||||
m_processor.result().print_statistics.total_seam_scarf_distance += static_cast<float>(seam_scarf_distance_mm);
|
||||
}
|
||||
|
||||
// BBS
|
||||
// Orca: share the post-extrusion nozzle position between wipe_inward and wipe_on_loops.
|
||||
const bool is_ccw = loop.is_counter_clockwise();
|
||||
|
||||
std::optional<Point> wipe_on_loops_dest;
|
||||
if (m_config.wipe_on_loops.value && paths.back().role() == erExternalPerimeter &&
|
||||
m_layer != nullptr && m_config.wall_loops.value > 1 && paths.front().size() >= 2 &&
|
||||
paths.back().polyline.points.size() >= 2)
|
||||
wipe_on_loops_dest = wipe_on_loops_destination(paths, scale_(nozzle_diameter), is_ccw, is_hole);
|
||||
|
||||
bool wipe_inward_applied = false;
|
||||
// Orca: store loop paths in print order because inward offsets use this orientation.
|
||||
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
|
||||
m_wipe.path = Polyline();
|
||||
for (ExtrusionPath &path : paths) {
|
||||
//BBS: Don't need to save duplicated point into wipe path
|
||||
if (!m_wipe.path.empty() && !path.empty() &&
|
||||
m_wipe.path.last_point() == Point(path.first_point().x(), path.first_point().y())) {
|
||||
// Convert Points3 to Points
|
||||
for (auto it = path.polyline.points.begin() + 1; it != path.polyline.points.end(); ++it)
|
||||
m_wipe.path.append(Point(it->x(), it->y()));
|
||||
} else
|
||||
m_wipe.path.append(path.polyline.to_polyline()); // TODO: don't limit wipe to last path
|
||||
m_wipe.update_path(paths);
|
||||
|
||||
// Orca: loop wipe paths retain print direction. Their material side is
|
||||
// therefore left for CCW contours and right for CW contours, with the
|
||||
// result inverted for holes. Only external perimeters are eligible.
|
||||
// Calibration overrides are applied during extrusion, after the region
|
||||
// context was created. Check the effective setting again at execution.
|
||||
if (m_config.wipe_inward && m_config.wipe_inward_distance.value > 0. &&
|
||||
wipe_support != nullptr && !wipe_support->inner_lines.empty() &&
|
||||
// A loop's role is its first path's role. An overhanging start must
|
||||
// not hide ordinary external-wall segments elsewhere in the loop.
|
||||
std::any_of(paths.begin(), paths.end(),
|
||||
[](const ExtrusionPath &path) { return is_external_perimeter(path.role()); }) &&
|
||||
m_wipe.path.points.size() >= 2) {
|
||||
// Orca: use the actual extrusion width from the path, not the config
|
||||
// value — outer_wall_line_width=0 (Auto) would make get_abs_value
|
||||
// return 0 and silently disable the feature, and Arachne may produce
|
||||
// a different width than the config default.
|
||||
const double outer_wall_line_width = paths.front().width;
|
||||
const double requested_offset = m_config.wipe_inward_distance.get_abs_value(outer_wall_line_width);
|
||||
const double offset_dist = scale_(std::min(requested_offset, outer_wall_line_width));
|
||||
if (offset_dist > SCALED_EPSILON) {
|
||||
const Point seam_start = paths.front().first_point();
|
||||
const Point seam_end = paths.back().last_point();
|
||||
const Point wipe_start = wipe_on_loops_dest.value_or(seam_end);
|
||||
const double max_wipe_length = scale_(FILAMENT_CONFIG(wipe_distance));
|
||||
// Orca: Wipe::wipe() replaces points[0] with last_pos and executes
|
||||
// from points[1]. The helper preserves that sentinel and atomically
|
||||
// replaces the remaining points, or leaves the path untouched.
|
||||
// Orca: a configured wall count does not guarantee that Arachne
|
||||
// generated an adjacent wall for this particular loop. Only
|
||||
// earlier entities are considered because later walls have
|
||||
// not been printed yet (for example with Outer/Inner order).
|
||||
// Inner walls determine the material side; every earlier wall
|
||||
// remains available to validate the executable wipe path.
|
||||
const double support_distance = scale_(std::max(nozzle_diameter, outer_wall_line_width));
|
||||
Polyline inward_path = m_wipe.path;
|
||||
if (offset_wipe_path_toward_support(
|
||||
inward_path, seam_start, seam_end, wipe_start,
|
||||
wipe_offset_direction(is_ccw, is_hole), offset_dist, max_wipe_length,
|
||||
wipe_support->inner_lines, wipe_support->printed_lines,
|
||||
m_wipe.path.lines(), support_distance)) {
|
||||
m_wipe.path = std::move(inward_path);
|
||||
wipe_inward_applied = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// make a little move inwards before leaving loop
|
||||
if (m_config.wipe_on_loops.value && paths.back().role() == erExternalPerimeter && m_layer != NULL && m_config.wall_loops.value > 1 && paths.front().size() >= 2 && paths.back().polyline.points.size() >= 3) {
|
||||
// detect angle between last and first segment
|
||||
// the side depends on the original winding order of the polygon (inwards for contours, outwards for holes)
|
||||
//FIXME improve the algorithm in case the loop is tiny.
|
||||
//FIXME improve the algorithm in case the loop is split into segments with a low number of points (see the Point b query).
|
||||
const Point3 &a3 = paths.front().polyline.points[1]; // second point
|
||||
Point a = Point(a3.x(), a3.y());
|
||||
const Point3 &b3 = *(paths.back().polyline.points.end()-3); // second to last point
|
||||
Point b = Point(b3.x(), b3.y());
|
||||
if (is_hole == loop.is_counter_clockwise()) {
|
||||
// swap points
|
||||
Point c = a; a = b; b = c;
|
||||
}
|
||||
|
||||
double angle = paths.front().first_point().ccw_angle(a, b) / 3;
|
||||
|
||||
// turn inwards if contour, turn outwards if hole
|
||||
if (is_hole == loop.is_counter_clockwise()) angle *= -1;
|
||||
|
||||
// create the destination point along the first segment and rotate it
|
||||
// we make sure we don't exceed the segment length because we don't know
|
||||
// the rotation of the second segment so we might cross the object boundary
|
||||
Vec2d p1 = paths.front().polyline.points.front().cast<double>().head<2>();
|
||||
Vec2d p2 = paths.front().polyline.points[1].cast<double>().head<2>();
|
||||
Vec2d v = p2 - p1;
|
||||
double nd = scale_(EXTRUDER_CONFIG(nozzle_diameter));
|
||||
double l2 = v.squaredNorm();
|
||||
// Shift by no more than a nozzle diameter.
|
||||
//FIXME Hiding the seams will not work nicely for very densely discretized contours!
|
||||
//BBS. shorten the travel distant before the wipe path
|
||||
double threshold = 0.2;
|
||||
Point pt = (p1 + v * threshold).cast<coord_t>();
|
||||
if (nd * nd < l2)
|
||||
pt = (p1 + threshold * v * (nd / sqrt(l2))).cast<coord_t>();
|
||||
//Point pt = ((nd * nd >= l2) ? (p1+v*0.4): (p1 + 0.2 * v * (nd / sqrt(l2)))).cast<coord_t>();
|
||||
const Point3 ¢er3 = paths.front().polyline.points.front();
|
||||
pt.rotate(angle, Point(center3.x(), center3.y()));
|
||||
// generate the travel move
|
||||
gcode += m_writer.extrude_to_xy(this->point_to_gcode(pt), 0, "move inwards before travel", true);
|
||||
// Orca: make the configured inward move before leaving the loop.
|
||||
if (wipe_on_loops_dest) {
|
||||
gcode += m_writer.extrude_to_xy(
|
||||
this->point_to_gcode(*wipe_on_loops_dest), 0, "move inwards before travel", true);
|
||||
this->set_last_pos(*wipe_on_loops_dest);
|
||||
}
|
||||
|
||||
// Execute the accepted path before another extrusion replaces it. Wiping
|
||||
// must not force retraction or Z-hop across a short travel to the next wall.
|
||||
// Ordinary travel planning decides whether to retract from the new position.
|
||||
if (wipe_inward_applied)
|
||||
gcode += m_wipe.wipe(*this, 0.);
|
||||
|
||||
return gcode;
|
||||
}
|
||||
|
||||
@@ -7519,21 +7588,9 @@ std::string GCode::extrude_multi_path(const ExtrusionMultiPath& multipath, const
|
||||
m_multi_flow_segment_path_pa_set = true;
|
||||
}
|
||||
|
||||
// BBS
|
||||
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
|
||||
m_wipe.path = Polyline();
|
||||
for (const ExtrusionPath &path : multipath.paths) {
|
||||
//BBS: Don't need to save duplicated point into wipe path
|
||||
if (!m_wipe.path.empty() && !path.empty() &&
|
||||
m_wipe.path.last_point() == Point(path.first_point().x(), path.first_point().y())) {
|
||||
// Convert Points3 to Points
|
||||
for (auto it = path.polyline.points.begin() + 1; it != path.polyline.points.end(); ++it)
|
||||
m_wipe.path.append(Point(it->x(), it->y()));
|
||||
} else
|
||||
m_wipe.path.append(path.polyline.to_polyline()); // TODO: don't limit wipe to last path
|
||||
}
|
||||
m_wipe.path.reverse();
|
||||
}
|
||||
// Orca: multipath wipes retrace the extrusion in reverse order.
|
||||
if (m_wipe.enable && FILAMENT_CONFIG(wipe))
|
||||
m_wipe.update_path(multipath.paths, true);
|
||||
|
||||
return gcode;
|
||||
}
|
||||
@@ -7541,14 +7598,15 @@ std::string GCode::extrude_multi_path(const ExtrusionMultiPath& multipath, const
|
||||
std::string GCode::extrude_entity(const ExtrusionEntity& entity,
|
||||
const std::string& description,
|
||||
double speed,
|
||||
const ExtrusionEntitiesPtr& region_perimeters)
|
||||
const ExtrusionEntitiesPtr& region_perimeters,
|
||||
const WipeInwardSupport* wipe_support)
|
||||
{
|
||||
if (const ExtrusionPath* path = dynamic_cast<const ExtrusionPath*>(&entity))
|
||||
return this->extrude_path(*path, description, speed);
|
||||
else if (const ExtrusionMultiPath* multipath = dynamic_cast<const ExtrusionMultiPath*>(&entity))
|
||||
return this->extrude_multi_path(*multipath, description, speed);
|
||||
else if (const ExtrusionLoop* loop = dynamic_cast<const ExtrusionLoop*>(&entity))
|
||||
return this->extrude_loop(*loop, description, speed, region_perimeters);
|
||||
return this->extrude_loop(*loop, description, speed, region_perimeters, nullptr, wipe_support);
|
||||
else
|
||||
throw Slic3r::InvalidArgument("Invalid argument supplied to extrude()");
|
||||
return "";
|
||||
@@ -7562,6 +7620,7 @@ std::string GCode::extrude_path(const ExtrusionPath& path, const std::string& de
|
||||
// description += ExtrusionEntity::role_to_string(path.role());
|
||||
std::string gcode = this->_extrude(path, description, speed);
|
||||
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
|
||||
m_wipe.reset_path();
|
||||
m_wipe.path = path.polyline.to_polyline();
|
||||
if (is_tree(this->config().support_type) && is_support(path.role())) {
|
||||
if ((m_wipe.path.first_point() - m_wipe.path.last_point()).cast<double>().norm() > scale_(0.2)) {
|
||||
@@ -7582,7 +7641,7 @@ std::string GCode::extrude_path(const ExtrusionPath& path, const std::string& de
|
||||
}
|
||||
|
||||
// Extrude perimeters: Decide where to put seams (hide or align seams).
|
||||
std::string GCode::extrude_perimeters(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, bool is_first_layer, bool is_infill_first)
|
||||
std::string GCode::extrude_perimeters(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, bool is_first_layer, bool is_infill_first, bool unsupported_loops_only)
|
||||
{
|
||||
std::string gcode;
|
||||
for (const ObjectByExtruder::Island::Region ®ion : by_region)
|
||||
@@ -7594,8 +7653,36 @@ std::string GCode::extrude_perimeters(const Print &print, const std::vector<Obje
|
||||
: (m_config.is_infill_first == is_infill_first);
|
||||
if (!should_print) continue;
|
||||
|
||||
for (const ExtrusionEntity* ee : region.perimeters)
|
||||
gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters);
|
||||
// Build the printed prefix once in emission order, scoped to this
|
||||
// region. Disabled or zero-length wipes need no support geometry.
|
||||
std::optional<WipeInwardSupport> wipe_support;
|
||||
if (m_wipe.enable && FILAMENT_CONFIG(wipe) && m_config.wipe_inward &&
|
||||
m_config.wipe_inward_distance.value > 0. &&
|
||||
scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON)
|
||||
wipe_support.emplace();
|
||||
|
||||
// ORCA: loops flagged as extruded in mid air, out of reach of the layer below, are held back
|
||||
// for a second pass after the infill that anchors them. Infill already precedes infill first walls.
|
||||
const bool defer_unsupported = !is_infill_first;
|
||||
auto waits_for_infill = [](const ExtrusionEntity *ee) {
|
||||
return ee->is_loop() && static_cast<const ExtrusionLoop *>(ee)->print_after_infill;
|
||||
};
|
||||
|
||||
// The deferred pass runs after the infill, so the loops the first pass emitted are
|
||||
// already down and belong in the prefix an inward wipe may land on.
|
||||
if (wipe_support && defer_unsupported && unsupported_loops_only)
|
||||
for (const ExtrusionEntity* ee : region.perimeters)
|
||||
if (!waits_for_infill(ee))
|
||||
wipe_support->append(*ee);
|
||||
|
||||
for (const ExtrusionEntity* ee : region.perimeters) {
|
||||
if (defer_unsupported && waits_for_infill(ee) != unsupported_loops_only)
|
||||
continue;
|
||||
gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters,
|
||||
wipe_support ? &*wipe_support : nullptr);
|
||||
if (wipe_support)
|
||||
wipe_support->append(*ee);
|
||||
}
|
||||
}
|
||||
return gcode;
|
||||
}
|
||||
@@ -7836,7 +7923,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
// path is 2D. But in slope lift case, lift z is done in travel_to function.
|
||||
// Add m_need_change_layer_lift_z when change_layer in case of no lift if m_last_pos is equal to path.first_point() by chance
|
||||
Point first_point = path.first_point();
|
||||
if (!m_last_pos_defined || m_last_pos.to_point() != first_point || m_need_change_layer_lift_z || slope_need_z_travel) {
|
||||
if (!m_last_pos_defined || m_last_pos.to_point() != first_point || m_need_change_layer_lift_z ||
|
||||
slope_need_z_travel) {
|
||||
const bool _last_pos_undefined = !m_last_pos_defined;
|
||||
|
||||
double z = DBL_MAX;
|
||||
@@ -9469,12 +9557,14 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
|
||||
if (old_filament_id_in_new_extruder == -1)
|
||||
wipe_volume = 0;
|
||||
else {
|
||||
wipe_volume = flush_matrix[old_filament_id_in_new_extruder * number_of_extruders + new_filament_id];
|
||||
size_t flush_idx = size_t(old_filament_id_in_new_extruder) * number_of_extruders + new_filament_id;
|
||||
wipe_volume = flush_idx < flush_matrix.size() ? flush_matrix[flush_idx] : 0.f;
|
||||
wipe_volume *= m_config.flush_multiplier.get_at(new_extruder_id);
|
||||
}
|
||||
}
|
||||
else {
|
||||
wipe_volume = flush_matrix[old_filament_id * number_of_extruders + new_filament_id];
|
||||
size_t flush_idx = size_t(old_filament_id) * number_of_extruders + new_filament_id;
|
||||
wipe_volume = flush_idx < flush_matrix.size() ? flush_matrix[flush_idx] : 0.f;
|
||||
wipe_volume *= m_config.flush_multiplier.get_at(new_extruder_id); // if is multi_extruder only use the fist extruder matrix
|
||||
}
|
||||
wipe_volume = std::max(0.f, wipe_volume-grab_purge_volume);
|
||||
|
||||
+21
-6
@@ -39,6 +39,7 @@ namespace Slic3r {
|
||||
|
||||
// Forward declarations.
|
||||
class GCode;
|
||||
struct WipeInwardSupport;
|
||||
|
||||
namespace CustomGCode{ struct Item; }
|
||||
struct PrintInstance;
|
||||
@@ -61,7 +62,7 @@ public:
|
||||
bool enable;
|
||||
Polyline path;
|
||||
|
||||
// Orca:
|
||||
// Orca: retraction portions emitted before, during, and after the wipe move.
|
||||
struct RetractionValues{
|
||||
double retraction_length_before_wipe = 0.;
|
||||
double retraction_length_during_wipe = 0.;
|
||||
@@ -73,8 +74,10 @@ public:
|
||||
void reset_path() { this->path = Polyline(); }
|
||||
std::string wipe(GCode &gcodegen, double length, bool toolchange = false, bool is_last = false);
|
||||
|
||||
// Orca:
|
||||
// Orca: calculate the retraction portions that can be emitted at wipe speed.
|
||||
RetractionValues calculateWipeRetractionLengths(GCode& gcodegen, bool toolchange);
|
||||
// Orca: rebuild the stored path while deduplicating shared path boundaries.
|
||||
void update_path(const ExtrusionPaths &paths, bool reverse = false);
|
||||
};
|
||||
|
||||
class WipeTowerIntegration {
|
||||
@@ -103,8 +106,13 @@ public:
|
||||
m_enable_wrapping_detection(print_config.enable_wrapping_detection && (print_config.wrapping_exclude_area.values.size() > 2) && (slice_used_filaments.size() <= 1)),
|
||||
m_is_first_print(true),
|
||||
m_print_config(&print_config),
|
||||
m_last_wipe_tower_print_z(print_config.z_offset.value)
|
||||
m_last_wipe_tower_print_z(print_config.z_offset.value),
|
||||
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(print_config))
|
||||
{
|
||||
// Precomputed rather than accumulated while emitting, so that the clearance validator and
|
||||
// the emitter cannot disagree about where the compacted tower sits on any given layer.
|
||||
if (m_sparse_layers_skipped)
|
||||
m_compacted_tower_z = compute_compacted_wipe_tower_z(tool_changes, float(print_config.z_offset.value));
|
||||
// initialize with the extruder offset of master extruder id
|
||||
m_extruder_offsets.resize(print_config.filament_map.size(), print_config.extruder_offset.get_at(print_config.master_extruder_id.value - 1));
|
||||
const auto& filament_map = print_config.filament_map.values; // 1 based idx
|
||||
@@ -164,6 +172,11 @@ private:
|
||||
float m_wipe_tower_depth;
|
||||
BoundingBoxf m_wipe_tower_bbx;
|
||||
Vec2f m_rib_offset{Vec2f(0, 0)};
|
||||
// wipe_tower_no_sparse_layers, as answered by the shared compaction rule rather than by the raw
|
||||
// option: smooth timelapse and wrapping detection keep a tower on every layer regardless.
|
||||
const bool m_sparse_layers_skipped;
|
||||
// Print z of the compacted tower per planned layer. Empty when the tower is not compacted.
|
||||
std::vector<float> m_compacted_tower_z;
|
||||
};
|
||||
|
||||
class ColorPrintColors
|
||||
@@ -430,14 +443,16 @@ private:
|
||||
std::string extrude_entity(const ExtrusionEntity& entity,
|
||||
const std::string& description = "",
|
||||
double speed = -1.,
|
||||
const ExtrusionEntitiesPtr& region_perimeters = ExtrusionEntitiesPtr());
|
||||
const ExtrusionEntitiesPtr& region_perimeters = ExtrusionEntitiesPtr(),
|
||||
const WipeInwardSupport* wipe_support = nullptr);
|
||||
// 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_loop(const ExtrusionLoop& loop,
|
||||
const std::string& description,
|
||||
double speed = -1.,
|
||||
const ExtrusionEntitiesPtr& region_perimeters = ExtrusionEntitiesPtr(),
|
||||
const Point* start_point = nullptr);
|
||||
const Point* start_point = nullptr,
|
||||
const WipeInwardSupport* wipe_support = nullptr);
|
||||
std::string extrude_multi_path(const ExtrusionMultiPath& multipath, const std::string& description = "", double speed = -1.);
|
||||
std::string extrude_path(const ExtrusionPath& path, const std::string& description = "", double speed = -1.);
|
||||
|
||||
@@ -519,7 +534,7 @@ private:
|
||||
// For sequential print, the instance of the object to be printing has to be defined.
|
||||
const size_t single_object_instance_idx);
|
||||
|
||||
std::string extrude_perimeters(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool is_first_layer, bool is_infill_first);
|
||||
std::string extrude_perimeters(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool is_first_layer, bool is_infill_first, bool unsupported_loops_only = false);
|
||||
std::string extrude_infill(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool ironing);
|
||||
std::string extrude_support(const ExtrusionEntityCollection& support_fills, const ExtrusionRole support_extrusion_role);
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ class FanMover
|
||||
private:
|
||||
const std::regex regex_fan_speed;
|
||||
const float nb_seconds_delay;
|
||||
const bool with_D_option;
|
||||
// Set from fan_speedup_time at the call site, but nothing here reads it.
|
||||
[[maybe_unused]] const bool with_D_option;
|
||||
const bool relative_e;
|
||||
const bool only_overhangs;
|
||||
const float kickstart;
|
||||
|
||||
@@ -1468,9 +1468,11 @@ void GCodeProcessor::run_post_process()
|
||||
|
||||
// Append a per-filament usage block at a filament change.
|
||||
auto handle_filament_change = [&](int filament_id, int cur_line_id, int nozzle_id) {
|
||||
// skip filament changes emitted inside the machine start / end gcode
|
||||
if (m_machine_start_gcode_end_line_id == (unsigned int) (-1) && (unsigned int) (cur_line_id) < m_machine_start_gcode_end_line_id ||
|
||||
m_machine_end_gcode_start_line_id != (unsigned int) (-1) && (unsigned int) (cur_line_id) > m_machine_end_gcode_start_line_id)
|
||||
// Skip filament changes emitted inside the machine start / end gcode. One forward pass assigns
|
||||
// the tag ids and tests them in the same loop, so inside the start gcode the end tag is unseen
|
||||
// and the id still holds the sentinel. That is why the first clause tests == and the second !=.
|
||||
if ((m_machine_start_gcode_end_line_id == (unsigned int) (-1) && (unsigned int) (cur_line_id) < m_machine_start_gcode_end_line_id) ||
|
||||
(m_machine_end_gcode_start_line_id != (unsigned int) (-1) && (unsigned int) (cur_line_id) > m_machine_end_gcode_start_line_id))
|
||||
return;
|
||||
if (!m_filament_blocks.empty())
|
||||
m_filament_blocks.back().upper_gcode_id = cur_line_id;
|
||||
@@ -2777,7 +2779,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
|
||||
std::map<int, std::map<int, GCodePosInfo>> gcode_path_pos; // object_id, filament_id, pos
|
||||
for (const GCodeProcessorResult::MoveVertex &move : m_result.moves) {
|
||||
// sometimes, the start line extrude was outside the edge of plate a little, this is allowed, so do not include into the gcode_path_pos
|
||||
if (move.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/)
|
||||
if (move.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/) {
|
||||
if (move.extrusion_role == ExtrusionRole::erCustom) {
|
||||
/*if (move.is_arc_move_with_interpolation_points()) {
|
||||
for (int i = 0; i < move.interpolation_points.size(); i++) {
|
||||
@@ -2799,6 +2801,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
|
||||
gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z = std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z,
|
||||
move.print_z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool valid = true;
|
||||
@@ -7593,8 +7596,8 @@ void GCodeProcessor::update_slice_warnings()
|
||||
if (used_filaments[idx] < m_result.required_nozzle_HRC.size())
|
||||
filament_hrc = m_result.required_nozzle_HRC[used_filaments[idx]];
|
||||
|
||||
int filament_extruder_id = m_filament_maps[used_filaments[idx]];
|
||||
int extruder_hrc = nozzle_hrc_lists[filament_extruder_id];
|
||||
int filament_extruder_id = used_filaments[idx] < m_filament_maps.size() ? m_filament_maps[used_filaments[idx]] : -1;
|
||||
int extruder_hrc = (filament_extruder_id >= 0 && (size_t) filament_extruder_id < nozzle_hrc_lists.size()) ? nozzle_hrc_lists[filament_extruder_id] : 0;
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": Check HRC: filament:%1%, hrc=%2%, extruder:%3%, hrc:%4%") % used_filaments[idx] % filament_hrc % filament_extruder_id % extruder_hrc;
|
||||
|
||||
|
||||
@@ -910,7 +910,7 @@ namespace Slic3r
|
||||
|
||||
unsigned int iterations = (1 << all_extruders.size());
|
||||
unsigned int final_state = iterations - 1;
|
||||
std::vector<std::vector<float>>cache(iterations, std::vector<float>(all_extruders.size(), 0x7fffffff));
|
||||
std::vector<std::vector<float>>cache(iterations, std::vector<float>(all_extruders.size(), std::numeric_limits<float>::max()));
|
||||
std::vector<std::vector<int>>prev(iterations, std::vector<int>(all_extruders.size(), -1));
|
||||
cache[1][0] = 0.;
|
||||
for (unsigned int state = 0; state < iterations; ++state) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "FilamentMixer.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "format.hpp"
|
||||
#include "I18N.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
@@ -82,8 +83,9 @@ bool check_filament_printable_after_group(const std::vector<unsigned int> &used_
|
||||
int printable_status = print_config->filament_printable.get_at(filament_id);
|
||||
int extruder_idx = filament_maps[filament_id];
|
||||
if (!(printable_status >> extruder_idx & 1)) {
|
||||
std::string extruder_name = extruder_idx == 0 ? _L("left") : _L("right");
|
||||
std::string error_msg = _L("Grouping error: ") + filament_type + _L(" can not be placed in the ") + extruder_name + _L(" nozzle");
|
||||
std::string error_msg = extruder_idx == 0 ?
|
||||
Slic3r::format(_L("Grouping error: %1% cannot be placed in the left nozzle"), filament_type) :
|
||||
Slic3r::format(_L("Grouping error: %1% cannot be placed in the right nozzle"), filament_type);
|
||||
throw Slic3r::RuntimeError(error_msg);
|
||||
}
|
||||
}
|
||||
@@ -1488,10 +1490,10 @@ static FilamentGroupContext build_filament_group_context(
|
||||
|
||||
auto machine_filament_info = build_machine_filaments(print->get_extruder_filament_info(), extruder_ams_counts, ignore_ext_filament);
|
||||
|
||||
std::vector<std::string> filament_types = print_config.filament_type.values;
|
||||
std::vector<std::string> filament_colours = print_config.filament_colour.values;
|
||||
std::vector<unsigned char> filament_is_support = print_config.filament_is_support.values;
|
||||
std::vector<std::string> filament_ids = print_config.filament_ids.values;
|
||||
// The grouping code walks filament_ids and indexes filament_info by the same position.
|
||||
std::vector<std::string> filament_ids = print_config.filament_ids.values;
|
||||
if (filament_ids.size() > filament_nums)
|
||||
filament_ids.resize(filament_nums);
|
||||
|
||||
FGMode fg_mode = mode == FilamentMapMode::fmmAutoForMatch ? FGMode::MatchMode : FGMode::FlushMode;
|
||||
context.model_info.flush_matrix = std::move(nozzle_flush_mtx);
|
||||
@@ -1500,11 +1502,14 @@ static FilamentGroupContext build_filament_group_context(
|
||||
context.model_info.filament_ids = filament_ids;
|
||||
context.model_info.unprintable_volumes = unprintable_volumes;
|
||||
|
||||
for (size_t idx = 0; idx < filament_types.size(); ++idx) {
|
||||
// Consumers index filament_info by filament id, so it must span the filament count: a partial
|
||||
// or legacy config can leave any of these arrays short, and get_at clamps.
|
||||
context.model_info.filament_info.reserve(filament_nums);
|
||||
for (size_t idx = 0; idx < filament_nums; ++idx) {
|
||||
FilamentGroupUtils::FilamentInfo info;
|
||||
info.color = filament_colours[idx];
|
||||
info.type = filament_types[idx];
|
||||
info.is_support = filament_is_support[idx];
|
||||
info.color = print_config.filament_colour.get_at(idx);
|
||||
info.type = print_config.filament_type.get_at(idx);
|
||||
info.is_support = print_config.filament_is_support.get_at(idx);
|
||||
context.model_info.filament_info.emplace_back(std::move(info));
|
||||
}
|
||||
|
||||
@@ -2732,6 +2737,28 @@ void ToolOrdering::enforce_mixed_component_order()
|
||||
}
|
||||
}
|
||||
|
||||
// Declared in ToolOrdering.hpp (exposed for unit testing).
|
||||
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders)
|
||||
{
|
||||
std::vector<unsigned int> order;
|
||||
for (const std::string& token : split_string(str, ',')) {
|
||||
try {
|
||||
size_t pos = 0;
|
||||
int filament = std::stoi(token, &pos); // stoi skips leading whitespace by itself
|
||||
// stoi stops at the first non-digit, so "2x" would parse as 2. Require the whole token to be
|
||||
// consumed (bar trailing whitespace) to drop it like any other garbage.
|
||||
if (token.find_first_not_of(" \t\r\n", pos) != std::string::npos)
|
||||
continue;
|
||||
if (filament >= 1 && (unsigned int)filament <= number_of_extruders
|
||||
&& std::find(order.begin(), order.end(), (unsigned int)(filament - 1)) == order.end())
|
||||
order.emplace_back((unsigned int)(filament - 1));
|
||||
} catch (const std::exception&) {
|
||||
// Not a number, ignore it.
|
||||
}
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
|
||||
{
|
||||
const PrintConfig* print_config = m_print_config_ptr;
|
||||
@@ -2829,11 +2856,41 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
|
||||
const bool use_cyclic_ordering =
|
||||
(print_config->toolchange_ordering == ToolChangeOrderingType::Cyclic);
|
||||
|
||||
// By default the first layer keeps its adhesion-optimized order (and any custom first layer
|
||||
// sequence); the cyclic sequence is only forced onto it when the user opts in.
|
||||
const bool cyclic_first_layer = use_cyclic_ordering && print_config->toolchange_cyclic_first_layer.value;
|
||||
|
||||
// Optional user defined cyclic sequence, given as 1-based filament numbers ("3,2,1,4"). Filaments
|
||||
// missing from it keep their ascending order after the listed ones, so a partial or bogus entry
|
||||
// still yields the default cyclic order.
|
||||
const std::vector<unsigned int> cyclic_order =
|
||||
use_cyclic_ordering ? parse_cyclic_order(print_config->toolchange_cyclic_order.value, number_of_extruders)
|
||||
: std::vector<unsigned int>();
|
||||
|
||||
// Reorder a layer's filaments (0-based) for cyclic ordering: ascending by default, or following the
|
||||
// user defined sequence when one was given. Filaments absent from the sequence keep ascending order
|
||||
// after the listed ones.
|
||||
auto apply_cyclic_order = [&cyclic_order](std::vector<unsigned int>& filaments) {
|
||||
std::sort(filaments.begin(), filaments.end());
|
||||
if (!cyclic_order.empty())
|
||||
std::stable_sort(filaments.begin(), filaments.end(), [&cyclic_order](unsigned int lhs, unsigned int rhs) {
|
||||
auto rank = [&cyclic_order](unsigned int filament) {
|
||||
return size_t(std::find(cyclic_order.begin(), cyclic_order.end(), filament) - cyclic_order.begin());
|
||||
};
|
||||
return rank(lhs) < rank(rhs);
|
||||
});
|
||||
};
|
||||
|
||||
// other_layers_seq: the layer_idx and extruder_idx are base on 1
|
||||
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering](int layer_idx, std::vector<int>& out_seq) -> bool {
|
||||
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering, cyclic_first_layer, &apply_cyclic_order](int layer_idx, std::vector<int>& out_seq) -> bool {
|
||||
if (!reorder_first_layer && layer_idx == 0) {
|
||||
out_seq.resize(first_layer_filaments.size());
|
||||
std::transform(first_layer_filaments.begin(), first_layer_filaments.end(), out_seq.begin(), [](auto item) {return item + 1; });
|
||||
// The first layer tool order is already decided (adhesion-optimized, plus any custom first
|
||||
// layer sequence). Only override it with the cyclic sequence when the user opted in.
|
||||
std::vector<unsigned int> ordered = first_layer_filaments;
|
||||
if (cyclic_first_layer)
|
||||
apply_cyclic_order(ordered);
|
||||
out_seq.resize(ordered.size());
|
||||
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) {return int(item) + 1; });
|
||||
return true;
|
||||
}
|
||||
for (size_t idx = other_layers_seqs.size() - 1; idx != size_t(-1); --idx) {
|
||||
@@ -2844,9 +2901,12 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
|
||||
}
|
||||
}
|
||||
|
||||
if (use_cyclic_ordering && layer_idx >= 0 && size_t(layer_idx) < layer_filaments.size()) {
|
||||
// Skip the first layer here (layer_idx == 0 only reaches this point on the reorder_first_layer
|
||||
// path) unless the user asked for cyclic order on it, so it keeps the default flush ordering.
|
||||
if (use_cyclic_ordering && layer_idx >= 0 && (layer_idx != 0 || cyclic_first_layer)
|
||||
&& size_t(layer_idx) < layer_filaments.size()) {
|
||||
std::vector<unsigned int> ordered = layer_filaments[size_t(layer_idx)];
|
||||
std::sort(ordered.begin(), ordered.end());
|
||||
apply_cyclic_order(ordered);
|
||||
out_seq.resize(ordered.size());
|
||||
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) { return int(item) + 1; });
|
||||
return true;
|
||||
@@ -3137,7 +3197,7 @@ void ToolOrdering::assign_custom_gcodes(const Print &print)
|
||||
// Skip all custom G-codes above this layer and skip all extruder switches.
|
||||
for (; custom_gcode_it != custom_gcode_per_print_z.gcodes.rend() && (
|
||||
(print_z_above > lt.print_z && custom_gcode_it->print_z > 0.5 * (lt.print_z + print_z_above))
|
||||
|| custom_gcode_it->type == CustomGCode::ToolChange); ++ custom_gcode_it);
|
||||
|| custom_gcode_it->type == CustomGCode::ToolChange); ++ custom_gcode_it) {}
|
||||
print_z_above = lt.print_z;
|
||||
if (custom_gcode_it == custom_gcode_per_print_z.gcodes.rend())
|
||||
// Custom G-codes were processed.
|
||||
|
||||
@@ -417,6 +417,11 @@ private:
|
||||
int most_used_extruder;
|
||||
};
|
||||
|
||||
// Parse the user defined cyclic toolchange sequence ("3,2 , 1 , 4") into 0-based filament indices.
|
||||
// Out-of-range entries, duplicates and non-numeric tokens are dropped, so a partially valid string
|
||||
// still orders the filaments it does name. Exposed for unit testing.
|
||||
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders);
|
||||
|
||||
} // namespace SLic3r
|
||||
|
||||
#endif /* slic3r_ToolOrdering_hpp_ */
|
||||
|
||||
@@ -0,0 +1,920 @@
|
||||
#include "WipePathHelpers.hpp"
|
||||
|
||||
#include "../AABBTreeLines.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <tuple>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void WipeInwardSupport::append(const ExtrusionEntity &entity)
|
||||
{
|
||||
const ExtrusionPaths *paths = nullptr;
|
||||
if (const auto *loop = dynamic_cast<const ExtrusionLoop *>(&entity))
|
||||
paths = &loop->paths;
|
||||
else if (const auto *multipath = dynamic_cast<const ExtrusionMultiPath *>(&entity))
|
||||
paths = &multipath->paths;
|
||||
|
||||
// A loop's role is its first path's role. An overhanging start must not
|
||||
// hide the ordinary inner-wall segments elsewhere in the same loop.
|
||||
const bool is_inner = paths ? std::any_of(paths->begin(), paths->end(),
|
||||
[](const ExtrusionPath &path) { return is_internal_perimeter(path.role()); }) :
|
||||
is_internal_perimeter(entity.role());
|
||||
const Lines lines = entity.as_polyline().lines();
|
||||
printed_lines.insert(printed_lines.end(), lines.begin(), lines.end());
|
||||
if (is_inner)
|
||||
inner_lines.insert(inner_lines.end(), lines.begin(), lines.end());
|
||||
}
|
||||
|
||||
// Orca: miter limit ratio. Matches DefaultMiterLimit from ClipperUtils.hpp.
|
||||
// When the miter join extends more than miter_limit * offset_dist from the
|
||||
// original vertex, the miter is replaced by a bevel join.
|
||||
static constexpr double miter_limit = 3.0;
|
||||
|
||||
// Orca: threshold for detecting near-reversal (backtracking spike).
|
||||
// Normalized dot product below this means the segments point in nearly
|
||||
// opposite directions (angle > ~172°). Offsetting such a path is unsafe.
|
||||
static constexpr double reversal_dot_threshold = -0.99;
|
||||
|
||||
// Orca: candidates pointing more than 60 degrees away from the selected inner
|
||||
// wall are too tangent to distinguish the material side reliably at a cusp.
|
||||
static constexpr double min_support_alignment = 0.5;
|
||||
|
||||
// Keep a scaled-coordinate rounding floor while allowing the tolerance to
|
||||
// follow the relevant offset or path length. Clearance allows a larger fraction.
|
||||
static double wipe_tolerance(double distance, double relative_tolerance = 0.1)
|
||||
{
|
||||
return std::max(4. * SCALED_EPSILON, relative_tolerance * distance);
|
||||
}
|
||||
|
||||
Point sample_path_at_distance(const ExtrusionPaths &paths, bool forward, double target)
|
||||
{
|
||||
assert(!paths.empty());
|
||||
if (paths.empty())
|
||||
return Point(0, 0);
|
||||
|
||||
double remaining = target;
|
||||
Point result = forward ? paths.front().first_point() : paths.back().last_point();
|
||||
for (int pi = forward ? 0 : (int)paths.size() - 1;
|
||||
pi >= 0 && pi < (int)paths.size() && remaining > 0.;
|
||||
pi += forward ? 1 : -1) {
|
||||
const Points3 &pts = paths[pi].polyline.points;
|
||||
for (int i = forward ? 0 : (int)pts.size() - 1;
|
||||
remaining > 0. && (forward ? i + 1 < (int)pts.size() : i > 0);
|
||||
i += forward ? 1 : -1) {
|
||||
const int j = forward ? i + 1 : i - 1;
|
||||
const Point cur(pts[i].x(), pts[i].y());
|
||||
const Point next(pts[j].x(), pts[j].y());
|
||||
const double segment_length = (next - cur).cast<double>().norm();
|
||||
if (segment_length < SCALED_EPSILON)
|
||||
continue;
|
||||
if (remaining <= segment_length) {
|
||||
const double ratio = remaining / segment_length;
|
||||
return Point(coord_t(cur.x() + ratio * (next.x() - cur.x())),
|
||||
coord_t(cur.y() + ratio * (next.y() - cur.y())));
|
||||
}
|
||||
remaining -= segment_length;
|
||||
result = next;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Orca: consecutive duplicates carry no path length and can be removed safely.
|
||||
// A reversal, however, is real travelled distance: removing its vertex would
|
||||
// replace a long backtracking wipe with a short, unrelated shortcut.
|
||||
static bool prepare_source(Points &pts)
|
||||
{
|
||||
pts.erase(std::unique(pts.begin(), pts.end()), pts.end());
|
||||
|
||||
if (pts.size() < 2)
|
||||
return false;
|
||||
|
||||
for (size_t i = 1; i + 1 < pts.size(); ++i) {
|
||||
const Vec2d v_prev = (pts[i] - pts[i - 1]).cast<double>();
|
||||
const Vec2d v_next = (pts[i + 1] - pts[i]).cast<double>();
|
||||
const double dot = v_prev.dot(v_next) / (v_prev.norm() * v_next.norm());
|
||||
if (dot < reversal_dot_threshold)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool build_offset_polyline(const Points &original, int dir, double offset_dist,
|
||||
Points &result, size_t &first_join_index)
|
||||
{
|
||||
if (original.size() < 2)
|
||||
return false;
|
||||
|
||||
// Orca: collapse all consecutive duplicates first, then reject any
|
||||
// backtracking in the cleaned path instead of replacing travelled distance
|
||||
// with a shortcut.
|
||||
Points source = original;
|
||||
if (! prepare_source(source))
|
||||
return false;
|
||||
|
||||
const size_t n = source.size();
|
||||
|
||||
// Orca: compute the perpendicular offset for segment i->i+1 as an infinite Line.
|
||||
auto offset_segment = [dir, offset_dist](const Point &a, const Point &b) -> Line {
|
||||
Vec2d v = (b - a).cast<double>();
|
||||
double len = v.norm();
|
||||
Vec2d perp(0, 0);
|
||||
if (len > SCALED_EPSILON)
|
||||
perp = Vec2d(-v.y(), v.x()) * (dir * offset_dist / len);
|
||||
return Line(Point(coord_t(a.x() + perp.x()), coord_t(a.y() + perp.y())),
|
||||
Point(coord_t(b.x() + perp.x()), coord_t(b.y() + perp.y())));
|
||||
};
|
||||
|
||||
result.clear();
|
||||
result.reserve(n);
|
||||
first_join_index = 0;
|
||||
|
||||
// Orca: the first point is perpendicular to the first segment.
|
||||
Line l_prev = offset_segment(source[0], source[1]);
|
||||
result.push_back(l_prev.a);
|
||||
|
||||
// Orca: use the analytic intersection of adjacent offset segments for a
|
||||
// miter join. Intersecting the already rounded Line endpoints amplifies
|
||||
// coordinate quantization when the source segments are nearly parallel.
|
||||
for (size_t i = 1; i + 1 < n; ++i) {
|
||||
Line l_next = offset_segment(source[i], source[i + 1]);
|
||||
const Vec2d previous = (source[i] - source[i - 1]).cast<double>().normalized();
|
||||
const Vec2d next = (source[i + 1] - source[i]).cast<double>().normalized();
|
||||
const double denominator = 1. + previous.dot(next);
|
||||
|
||||
bool need_bevel = denominator <= EPSILON;
|
||||
Point pt;
|
||||
if (! need_bevel) {
|
||||
const Vec2d previous_normal(-previous.y(), previous.x());
|
||||
const Vec2d next_normal(-next.y(), next.x());
|
||||
const Vec2d miter = (previous_normal + next_normal) * (dir * offset_dist / denominator);
|
||||
if (miter.norm() > miter_limit * offset_dist) {
|
||||
need_bevel = true;
|
||||
} else {
|
||||
pt = Point(coord_t(source[i].x() + miter.x()),
|
||||
coord_t(source[i].y() + miter.y()));
|
||||
}
|
||||
}
|
||||
|
||||
if (need_bevel) {
|
||||
result.push_back(l_prev.b);
|
||||
if (l_next.a != result.back())
|
||||
result.push_back(l_next.a);
|
||||
} else {
|
||||
result.push_back(pt);
|
||||
}
|
||||
if (i == 1)
|
||||
first_join_index = result.size() - 1;
|
||||
l_prev = l_next;
|
||||
}
|
||||
|
||||
// Orca: the last point is perpendicular to the last segment.
|
||||
result.push_back(l_prev.b);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int wipe_offset_direction(bool is_ccw, bool is_hole)
|
||||
{
|
||||
const int loop_inside = is_ccw ? +1 : -1;
|
||||
return is_hole ? -loop_inside : loop_inside;
|
||||
}
|
||||
|
||||
static bool starts_by_backtracking(const Polyline &path, Point actual_start)
|
||||
{
|
||||
if (path.points.size() < 3)
|
||||
return false;
|
||||
// Orca: points[0] is only a storage sentinel; use the nozzle position for
|
||||
// the executable connector, particularly after a wipe_on_loops pre-move.
|
||||
const Vec2d connector = (path.points[1] - actual_start).cast<double>();
|
||||
const Vec2d outgoing = (path.points[2] - path.points[1]).cast<double>();
|
||||
// An inward connector may be perpendicular to the outgoing offset edge.
|
||||
// Rounded joins must not turn that right angle into a false backtrack.
|
||||
return connector.dot(outgoing) < -4. * SCALED_EPSILON * outgoing.norm();
|
||||
}
|
||||
|
||||
// Orca: sample the outgoing perimeter without copying or clipping its full loop.
|
||||
static Point sample_polyline_at_distance(const Polyline &polyline, double target)
|
||||
{
|
||||
assert(! polyline.points.empty());
|
||||
Point result = polyline.first_point();
|
||||
for (size_t i = 1; i < polyline.points.size() && target > 0.; ++i) {
|
||||
const Vec2d segment = (polyline.points[i] - result).cast<double>();
|
||||
const double length = segment.norm();
|
||||
if (length <= SCALED_EPSILON)
|
||||
continue;
|
||||
if (target <= length)
|
||||
return (result.cast<double>() + segment * (target / length)).cast<coord_t>();
|
||||
target -= length;
|
||||
result = polyline.points[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Orca: convert an executable path into Wipe::wipe()'s stored representation.
|
||||
// The first point is a dummy replaced by the actual nozzle position, while the
|
||||
// remaining points are clipped to the configured wipe distance.
|
||||
static bool store_wipe_path(Polyline &destination, Point seam_start,
|
||||
Polyline actual_path, double max_wipe_length)
|
||||
{
|
||||
if (actual_path.points.size() < 2 || max_wipe_length <= SCALED_EPSILON)
|
||||
return false;
|
||||
|
||||
const double actual_length = actual_path.length();
|
||||
if (actual_length <= SCALED_EPSILON)
|
||||
return false;
|
||||
if (actual_length - max_wipe_length > SCALED_EPSILON)
|
||||
actual_path.clip_end(actual_length - max_wipe_length);
|
||||
if (actual_path.points.size() < 2)
|
||||
return false;
|
||||
for (size_t i = 1; i < actual_path.points.size(); ++i)
|
||||
if (actual_path.points[i - 1] == actual_path.points[i])
|
||||
return false;
|
||||
|
||||
Polyline stored_path;
|
||||
stored_path.points.reserve(actual_path.points.size());
|
||||
stored_path.points.push_back(seam_start);
|
||||
stored_path.points.insert(stored_path.points.end(), actual_path.points.begin() + 1, actual_path.points.end());
|
||||
stored_path.reset_to_linear_move();
|
||||
destination = std::move(stored_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool offset_wipe_path(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||
int dir, double offset_dist, double max_wipe_length)
|
||||
{
|
||||
assert(dir == +1 || dir == -1);
|
||||
assert(offset_dist > 0);
|
||||
if (polyline.points.empty() || polyline.first_point() != seam_start ||
|
||||
max_wipe_length <= SCALED_EPSILON)
|
||||
return false;
|
||||
|
||||
const Polyline original = polyline;
|
||||
const double original_length = original.length();
|
||||
if (original_length <= SCALED_EPSILON)
|
||||
return false;
|
||||
|
||||
double source_length = std::min(original_length, max_wipe_length);
|
||||
for (;;) {
|
||||
Polyline source = original;
|
||||
const double clip_distance = original_length - source_length;
|
||||
if (clip_distance > SCALED_EPSILON)
|
||||
source.clip_end(clip_distance);
|
||||
|
||||
Points wrapped_source;
|
||||
wrapped_source.reserve(source.points.size() + 1);
|
||||
if (seam_start == seam_end) {
|
||||
// Orca: the stored loop is open at seam_start even when the seam gap is
|
||||
// zero. Prepend the closing edge so build_offset_polyline() creates
|
||||
// the proper join between that edge and the first outgoing edge,
|
||||
// instead of leaving the first offset point on the closing wall.
|
||||
size_t closing_index = original.points.size();
|
||||
while (closing_index > 0 && original.points[closing_index - 1] == seam_start)
|
||||
--closing_index;
|
||||
if (closing_index == 0)
|
||||
return false; // Orca: the entire path is a single point.
|
||||
wrapped_source.push_back(original.points[closing_index - 1]);
|
||||
} else {
|
||||
// Orca: use the unextruded seam-gap edge to determine the incoming
|
||||
// direction at the seam. Its offset is construction geometry only;
|
||||
// wiping along it would create a Z-shaped detour before the outgoing
|
||||
// perimeter offset.
|
||||
wrapped_source.push_back(seam_end);
|
||||
}
|
||||
wrapped_source.insert(wrapped_source.end(), source.points.begin(), source.points.end());
|
||||
|
||||
Points offset_points;
|
||||
size_t first_join_index = 0;
|
||||
if (! build_offset_polyline(wrapped_source, dir, offset_dist, offset_points, first_join_index) ||
|
||||
first_join_index == 0 || first_join_index >= offset_points.size())
|
||||
return false;
|
||||
// Orca: discard the offset of the prepended edge and, for a bevel, its
|
||||
// incoming endpoint. The executable wipe starts at the seam join and
|
||||
// then follows only the already printed outgoing perimeter.
|
||||
offset_points.erase(offset_points.begin(), offset_points.begin() + first_join_index);
|
||||
|
||||
Polyline actual_path;
|
||||
actual_path.points.reserve(offset_points.size() + 1);
|
||||
actual_path.points.push_back(wipe_start);
|
||||
actual_path.points.insert(actual_path.points.end(), offset_points.begin(), offset_points.end());
|
||||
|
||||
// A loop pre-move may advance past an otherwise valid offset join.
|
||||
// Enter at the nozzle's projection instead of returning to the join.
|
||||
// Do not repair a join that already backtracks across the seam gap;
|
||||
// the caller must still validate wall crossings, material side and support.
|
||||
if (seam_start != seam_end && wipe_start != seam_start && wipe_start != seam_end &&
|
||||
starts_by_backtracking(actual_path, wipe_start) && ! starts_by_backtracking(actual_path, seam_end)) {
|
||||
size_t entry = 1;
|
||||
while (entry + 1 < actual_path.points.size()) {
|
||||
const Vec2d edge = (actual_path.points[entry + 1] - actual_path.points[entry]).cast<double>();
|
||||
const double projection = (wipe_start - actual_path.points[entry]).cast<double>().dot(edge);
|
||||
if (projection <= 0.)
|
||||
break;
|
||||
if (projection < edge.squaredNorm()) {
|
||||
actual_path.points[entry] = (actual_path.points[entry].cast<double>() +
|
||||
edge * (projection / edge.squaredNorm())).cast<coord_t>();
|
||||
break;
|
||||
}
|
||||
++entry;
|
||||
}
|
||||
actual_path.points.erase(actual_path.points.begin() + 1, actual_path.points.begin() + entry);
|
||||
}
|
||||
|
||||
if (seam_start != seam_end && wipe_start == seam_end &&
|
||||
starts_by_backtracking(actual_path, wipe_start)) {
|
||||
// Orca: a wide seam gap or a sharp cusp may put the first miter
|
||||
// behind its outgoing edge. Reject this offset candidate so the
|
||||
// caller can try the opposite side or the translated fallback.
|
||||
return false;
|
||||
}
|
||||
|
||||
const double actual_length = actual_path.length();
|
||||
const bool source_exhausted = original_length - source_length <= SCALED_EPSILON;
|
||||
if (actual_length + SCALED_EPSILON < max_wipe_length && ! source_exhausted) {
|
||||
// Orca: offset joins may shorten the path at every corner. Grow the
|
||||
// source until the executable offset path, not a heuristic source
|
||||
// margin, reaches the configured wipe distance.
|
||||
const double deficit = max_wipe_length - actual_length;
|
||||
const double next_length = std::min(original_length,
|
||||
source_length + std::max(deficit, 2. * SCALED_EPSILON));
|
||||
if (next_length - source_length <= SCALED_EPSILON)
|
||||
return false;
|
||||
source_length = next_length;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Orca: unlike an extruded offset, a wipe may safely cross or retrace the
|
||||
// just-printed perimeter. The caller validates the complete executable
|
||||
// path against current and earlier printed perimeter geometry.
|
||||
return store_wipe_path(polyline, seam_start, std::move(actual_path), max_wipe_length);
|
||||
}
|
||||
}
|
||||
|
||||
static bool translated_wipe_path(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||
const Vec2d &translation, double max_wipe_length)
|
||||
{
|
||||
if (translation.norm() <= SCALED_EPSILON || max_wipe_length <= SCALED_EPSILON)
|
||||
return false;
|
||||
|
||||
const Polyline original = polyline;
|
||||
Polyline actual_path;
|
||||
actual_path.points.reserve(original.points.size() + 2);
|
||||
actual_path.points.push_back(wipe_start);
|
||||
|
||||
const auto append_translated = [&actual_path, &translation](const Point &point) {
|
||||
const Point translated = (point.cast<double>() + translation).cast<coord_t>();
|
||||
if (translated != actual_path.points.back())
|
||||
actual_path.points.push_back(translated);
|
||||
};
|
||||
|
||||
// Orca: translate the seam join directly. Translating seam_end and then
|
||||
// following the unextruded gap back to seam_start makes the wipe double
|
||||
// back whenever a gap ends near a sharp corner.
|
||||
append_translated(seam_start);
|
||||
for (const Point &point : original.points)
|
||||
append_translated(point);
|
||||
|
||||
if (seam_start != seam_end && wipe_start == seam_end &&
|
||||
starts_by_backtracking(actual_path, wipe_start)) {
|
||||
// Orca: at a wide gap next to a cusp, the translated seam join may
|
||||
// lie behind the outgoing edge. Prefer a shorter local inward move
|
||||
// at the actual extrusion end over a longer lightning-shaped wipe.
|
||||
actual_path.points.resize(1);
|
||||
append_translated(seam_end);
|
||||
}
|
||||
|
||||
return store_wipe_path(polyline, seam_start, std::move(actual_path), max_wipe_length);
|
||||
}
|
||||
|
||||
// A segment whose endpoints lie within one line's distance capsule is fully
|
||||
// supported, since that capsule is convex. Subdivide only when support changes
|
||||
// between lines; fixed-distance sampling can miss an unsupported gap.
|
||||
static bool segment_is_supported(Point start, Point end,
|
||||
const AABBTreeLines::LinesDistancer<Line> &distancer,
|
||||
double max_distance)
|
||||
{
|
||||
const Point midpoint = ((start.cast<double>() + end.cast<double>()) * 0.5).cast<coord_t>();
|
||||
const auto [distance, line_index, nearest] = distancer.distance_from_lines_extra<false>(midpoint);
|
||||
if (distance > max_distance)
|
||||
return false;
|
||||
|
||||
const Line &line = distancer.get_line(line_index);
|
||||
if (line.distance_to(start) <= max_distance && line.distance_to(end) <= max_distance)
|
||||
return true;
|
||||
if (distancer.distance_from_lines<false>(start) > max_distance ||
|
||||
distancer.distance_from_lines<false>(end) > max_distance)
|
||||
return false;
|
||||
|
||||
// Conservatively reject an unresolved transition at coordinate precision.
|
||||
if ((end - start).cast<double>().norm() <= SCALED_EPSILON)
|
||||
return false;
|
||||
return segment_is_supported(start, midpoint, distancer, max_distance) &&
|
||||
segment_is_supported(midpoint, end, distancer, max_distance);
|
||||
}
|
||||
|
||||
std::optional<double> wipe_path_support_score(
|
||||
const Polyline &polyline, Point wipe_start,
|
||||
const AABBTreeLines::LinesDistancer<Line> &target_distancer,
|
||||
const AABBTreeLines::LinesDistancer<Line> &all_support_distancer,
|
||||
double max_distance)
|
||||
{
|
||||
if (polyline.points.size() < 2 || target_distancer.get_lines().empty() || max_distance <= 0)
|
||||
return std::nullopt;
|
||||
|
||||
// Orca: require a local neighbour, not merely an earlier perimeter elsewhere in
|
||||
// the region. At a convex corner, an inner wall's miter is farther from the
|
||||
// external seam than its normal wall spacing, so allow the same bounded miter
|
||||
// reach as the offset construction without accepting a remote island.
|
||||
if (target_distancer.distance_from_lines<false>(wipe_start) >
|
||||
miter_limit * max_distance + 4. * SCALED_EPSILON)
|
||||
return std::nullopt;
|
||||
|
||||
Point previous = wipe_start;
|
||||
for (size_t i = 1; i < polyline.points.size(); ++i) {
|
||||
// Orca: a tightly curved inward path may cross back over the current wall.
|
||||
// This is safe for a non-extruding wipe as long as the complete path
|
||||
// remains over current or earlier printed perimeter geometry.
|
||||
// Allow the same coordinate-rounding tolerance at every point, including
|
||||
// the actual start substituted for the stored sentinel.
|
||||
if (! segment_is_supported(previous, polyline.points[i], all_support_distancer,
|
||||
max_distance + 4. * SCALED_EPSILON))
|
||||
return std::nullopt;
|
||||
previous = polyline.points[i];
|
||||
}
|
||||
|
||||
// Orca: decide direction at the seam. Scoring the complete path may select
|
||||
// the wrong initial side when two contours converge and the later prefix
|
||||
// happens to run closer to unrelated support.
|
||||
return target_distancer.distance_from_lines<false>(polyline.points[1]);
|
||||
}
|
||||
|
||||
static bool initial_connector_is_clear(
|
||||
const Polyline &polyline, Point wipe_start, Point seam_start,
|
||||
AABBTreeLines::LinesDistancer<Line> ¤t_perimeter_distancer,
|
||||
double contact_tolerance)
|
||||
{
|
||||
if (polyline.points.size() < 2 || polyline.points[1] == wipe_start)
|
||||
return false;
|
||||
|
||||
// Orca: without a seam gap, the connector necessarily starts at the wall
|
||||
// and a self-touching cusp may share that same endpoint on several edges.
|
||||
if (seam_start == wipe_start)
|
||||
return true;
|
||||
|
||||
const Line connector(wipe_start, polyline.points[1]);
|
||||
const auto intersections = current_perimeter_distancer.intersections_with_line<false>(connector);
|
||||
for (const auto &intersection : intersections) {
|
||||
if ((intersection.first - wipe_start).cast<double>().norm() > contact_tolerance)
|
||||
return false;
|
||||
}
|
||||
|
||||
Point closest;
|
||||
// Orca: integer offset joins may miss the exact seam-start coordinate by
|
||||
// a few microns. Treat a close pass through that point as retracing the
|
||||
// external wall, but keep the unavoidable contact at the actual start.
|
||||
if (connector.distance_to_squared(seam_start, &closest) <= contact_tolerance * contact_tolerance &&
|
||||
(closest - wipe_start).cast<double>().norm() > contact_tolerance)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::optional<Vec2d> support_offset_at_start(
|
||||
const Polyline &source, Point local_origin, bool disambiguate_branch,
|
||||
AABBTreeLines::LinesDistancer<Line> &support_distancer,
|
||||
double max_support_distance)
|
||||
{
|
||||
if (source.points.size() < 2)
|
||||
return std::nullopt;
|
||||
|
||||
// Orca: a nonzero gap may put the seam beside the wrong branch of a cusp.
|
||||
// Sample farther along the path to identify its actual neighbouring wall.
|
||||
const Point support_query = disambiguate_branch ?
|
||||
sample_polyline_at_distance(source, 2. * max_support_distance) : source.first_point();
|
||||
const auto nearest_result = support_distancer.distance_from_lines_extra<false>(support_query);
|
||||
const Line &nearest_line = support_distancer.get_line(std::get<1>(nearest_result));
|
||||
Vec2d sampled_offset = std::get<2>(nearest_result) - support_query.cast<double>();
|
||||
|
||||
if (disambiguate_branch) {
|
||||
// Orca: an endpoint projection also contains distance along the support
|
||||
// segment. Remove that tangent component before comparing wall sides.
|
||||
const Vec2d support_edge = (nearest_line.b - nearest_line.a).cast<double>();
|
||||
if (support_edge.norm() > SCALED_EPSILON) {
|
||||
const Vec2d support_tangent = support_edge.normalized();
|
||||
sampled_offset -= support_tangent * sampled_offset.dot(support_tangent);
|
||||
}
|
||||
}
|
||||
if (sampled_offset.norm() <= SCALED_EPSILON)
|
||||
return std::nullopt;
|
||||
|
||||
if (! disambiguate_branch)
|
||||
return sampled_offset;
|
||||
|
||||
// Orca: find the local point on the same material-side branch. Using the
|
||||
// sampled point itself would add the distance already travelled along the
|
||||
// perimeter and turn a normal transition into a long diagonal move.
|
||||
const Vec2d sampled_direction = sampled_offset.normalized();
|
||||
Vec2d local_offset = sampled_offset;
|
||||
double best_local_score = std::numeric_limits<double>::infinity();
|
||||
for (size_t line_index : support_distancer.all_lines_in_radius(
|
||||
local_origin, 2. * max_support_distance + 4. * SCALED_EPSILON)) {
|
||||
Point local_support;
|
||||
const Line &line = support_distancer.get_line(line_index);
|
||||
const double distance_squared = line.distance_to_squared(local_origin, &local_support);
|
||||
const Vec2d candidate_offset = local_support.cast<double>() - local_origin.cast<double>();
|
||||
const double candidate_distance = std::sqrt(distance_squared);
|
||||
if (candidate_distance <= SCALED_EPSILON)
|
||||
continue;
|
||||
const double alignment = candidate_offset.normalized().dot(sampled_direction);
|
||||
if (alignment < min_support_alignment)
|
||||
continue;
|
||||
const double score = candidate_distance / alignment;
|
||||
if (score < best_local_score) {
|
||||
best_local_score = score;
|
||||
local_offset = candidate_offset;
|
||||
}
|
||||
}
|
||||
return local_offset;
|
||||
}
|
||||
|
||||
static double executable_path_length(const Polyline &stored_path, Point wipe_start)
|
||||
{
|
||||
if (stored_path.points.size() < 2)
|
||||
return 0.;
|
||||
|
||||
// Orca: points[0] is the storage sentinel, so measure the first segment
|
||||
// from the actual nozzle position and the remaining stored segments normally.
|
||||
double length = (stored_path.points[1] - wipe_start).cast<double>().norm();
|
||||
for (size_t index = 2; index < stored_path.points.size(); ++index)
|
||||
length += (stored_path.points[index] - stored_path.points[index - 1]).cast<double>().norm();
|
||||
return length;
|
||||
}
|
||||
|
||||
static Lines material_side_support_lines(const Polyline &path, Point seam, int preferred_dir,
|
||||
const Lines &support_lines)
|
||||
{
|
||||
if (path.points.size() < 4 || path.first_point() != path.last_point())
|
||||
return {};
|
||||
|
||||
// Orca: the bisector of the incoming and outgoing material-side normals is
|
||||
// a local side test that remains valid for globally self-touching Arachne
|
||||
// contours. Ignore repeated seam points when obtaining both tangents.
|
||||
const auto outgoing_it = std::find_if(
|
||||
path.points.begin() + 1, path.points.end(), [seam](const Point &point) { return point != seam; });
|
||||
const auto incoming_it = std::find_if(
|
||||
path.points.rbegin() + 1, path.points.rend(), [seam](const Point &point) { return point != seam; });
|
||||
if (outgoing_it == path.points.end() || incoming_it == path.points.rend())
|
||||
return {};
|
||||
|
||||
const Vec2d outgoing = (*outgoing_it - seam).cast<double>().normalized();
|
||||
const Vec2d incoming = (seam - *incoming_it).cast<double>().normalized();
|
||||
const Vec2d material_direction =
|
||||
(Vec2d(-outgoing.y(), outgoing.x()) + Vec2d(-incoming.y(), incoming.x())) * preferred_dir;
|
||||
if (material_direction.norm() <= EPSILON)
|
||||
return {};
|
||||
|
||||
Lines result;
|
||||
result.reserve(support_lines.size());
|
||||
for (const Line &line : support_lines) {
|
||||
Point closest;
|
||||
line.distance_to_squared(seam, &closest);
|
||||
if ((closest - seam).cast<double>().dot(material_direction) > SCALED_EPSILON)
|
||||
result.push_back(line);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool wipe_path_stays_on_material_side(
|
||||
const Polyline &path, Point path_start, const Vec2d &support_direction,
|
||||
const AABBTreeLines::LinesDistancer<Line> &target_perimeter_distancer,
|
||||
const AABBTreeLines::LinesDistancer<Line> ¤t_perimeter_distancer,
|
||||
double effective_offset, bool require_clearance)
|
||||
{
|
||||
if (path.points.size() < 2 || support_direction.norm() <= EPSILON ||
|
||||
target_perimeter_distancer.get_lines().empty() || current_perimeter_distancer.get_lines().empty() ||
|
||||
effective_offset <= SCALED_EPSILON)
|
||||
return false;
|
||||
|
||||
const Vec2d initial_offset = (path.points[1] - path_start).cast<double>();
|
||||
if (initial_offset.norm() <= SCALED_EPSILON ||
|
||||
initial_offset.normalized().dot(support_direction.normalized()) < min_support_alignment)
|
||||
return false;
|
||||
// Orca: after the connector has left the extrusion endpoint, an inward
|
||||
// offset must retain most of its requested clearance from the current
|
||||
// external wall. Otherwise a tight turn may send an initially correct path
|
||||
// back onto that wall, or make the opposite-side candidate look supported.
|
||||
const double clearance_tolerance = wipe_tolerance(effective_offset, 0.25);
|
||||
const double minimum_clearance = effective_offset - clearance_tolerance;
|
||||
const Lines &lines = current_perimeter_distancer.get_lines();
|
||||
const auto left_normal = [](const Line &line) -> Vec2d {
|
||||
const Vec2d edge = (line.b - line.a).cast<double>();
|
||||
if (edge.norm() <= SCALED_EPSILON)
|
||||
return Vec2d::Zero();
|
||||
return Vec2d(-edge.y(), edge.x()).normalized();
|
||||
};
|
||||
const auto on_material_side = [&](const Point &point, bool check_clearance) {
|
||||
const auto [distance, line_index, nearest] =
|
||||
current_perimeter_distancer.distance_from_lines_extra<false>(point);
|
||||
if (line_index >= lines.size())
|
||||
return false;
|
||||
const Line &line = lines[line_index];
|
||||
Vec2d normal = left_normal(line);
|
||||
// At a shared vertex use both incident edges, so the result does not
|
||||
// depend on which equally close edge the AABB query happens to return.
|
||||
const Line &previous = lines[(line_index + lines.size() - 1) % lines.size()];
|
||||
const Line &next = lines[(line_index + 1) % lines.size()];
|
||||
if ((nearest - line.a.cast<double>()).norm() <= SCALED_EPSILON && previous.b == line.a)
|
||||
normal += left_normal(previous);
|
||||
if ((nearest - line.b.cast<double>()).norm() <= SCALED_EPSILON && next.a == line.b)
|
||||
normal += left_normal(next);
|
||||
if (normal.norm() <= EPSILON)
|
||||
return false;
|
||||
|
||||
// An open or self-touching wall has no reliable polygon-wide sign.
|
||||
// Orient its local normal toward the neighbouring printed inner wall,
|
||||
// then test the candidate on that side at every sample.
|
||||
normal.normalize();
|
||||
const Point wall_point = nearest.cast<coord_t>();
|
||||
const Vec2d support_point = std::get<2>(
|
||||
target_perimeter_distancer.distance_from_lines_extra<false>(wall_point));
|
||||
const double support_side = (support_point - nearest).dot(normal);
|
||||
if (std::abs(support_side) <= 4. * SCALED_EPSILON)
|
||||
return false;
|
||||
const double side = (point.cast<double>() - nearest).dot(normal) * (support_side > 0. ? 1. : -1.);
|
||||
return side >= -4. * SCALED_EPSILON &&
|
||||
(! check_clearance || distance + 4. * SCALED_EPSILON >= minimum_clearance);
|
||||
};
|
||||
|
||||
Point previous = path.points[1];
|
||||
if (! on_material_side(previous, require_clearance))
|
||||
return false;
|
||||
for (size_t index = 2; index < path.points.size(); ++index) {
|
||||
const Vec2d segment = (path.points[index] - previous).cast<double>();
|
||||
const size_t samples = std::max<size_t>(1, size_t(std::ceil(segment.norm() / effective_offset)));
|
||||
for (size_t sample = 1; sample <= samples; ++sample) {
|
||||
const Point point = (previous.cast<double>() +
|
||||
segment * (double(sample) / double(samples))).cast<coord_t>();
|
||||
if (! on_material_side(point, require_clearance))
|
||||
return false;
|
||||
}
|
||||
previous = path.points[index];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool offset_wipe_path_toward_support(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||
int preferred_dir, double offset_dist, double max_wipe_length,
|
||||
const Lines &target_perimeter_lines, const Lines &printed_perimeter_lines,
|
||||
const Lines ¤t_perimeter_lines,
|
||||
double max_support_distance)
|
||||
{
|
||||
assert(preferred_dir == +1 || preferred_dir == -1);
|
||||
if (polyline.points.size() < 2 || target_perimeter_lines.empty() || current_perimeter_lines.empty() ||
|
||||
offset_dist <= SCALED_EPSILON ||
|
||||
max_wipe_length <= SCALED_EPSILON || max_support_distance <= SCALED_EPSILON)
|
||||
return false;
|
||||
|
||||
Lines material_support_lines;
|
||||
const Lines *candidate_support_lines = &target_perimeter_lines;
|
||||
if (seam_start == seam_end) {
|
||||
// Orca: another contour may have a geometrically closer inner wall on
|
||||
// this loop's air side. Restrict zero-gap support using the local seam
|
||||
// normals before choosing the nearest wall.
|
||||
material_support_lines = material_side_support_lines(
|
||||
polyline, seam_start, preferred_dir, target_perimeter_lines);
|
||||
if (material_support_lines.empty())
|
||||
return false;
|
||||
candidate_support_lines = &material_support_lines;
|
||||
}
|
||||
|
||||
AABBTreeLines::LinesDistancer<Line> support_distancer(*candidate_support_lines);
|
||||
const std::optional<Vec2d> support_offset = support_offset_at_start(
|
||||
polyline, seam_end, seam_start != seam_end,
|
||||
support_distancer, max_support_distance);
|
||||
if (! support_offset)
|
||||
return false;
|
||||
const Vec2d toward_support = *support_offset;
|
||||
const double local_support_distance = toward_support.norm();
|
||||
const double effective_offset = std::min(offset_dist, local_support_distance);
|
||||
if (effective_offset <= SCALED_EPSILON)
|
||||
return false;
|
||||
const Vec2d support_direction = toward_support / local_support_distance;
|
||||
|
||||
// Orca: every candidate is validated against the same generated geometry.
|
||||
// Build these AABB trees once per loop instead of rebuilding them for each
|
||||
// preferred, alternate, translated, direct, or reversed candidate.
|
||||
Lines all_support_lines = printed_perimeter_lines;
|
||||
all_support_lines.insert(all_support_lines.end(), current_perimeter_lines.begin(), current_perimeter_lines.end());
|
||||
AABBTreeLines::LinesDistancer<Line> all_support_distancer(std::move(all_support_lines));
|
||||
AABBTreeLines::LinesDistancer<Line> current_perimeter_distancer(current_perimeter_lines);
|
||||
|
||||
// Orca: allow only the contact needed to leave the extrusion endpoint. A
|
||||
// connector that meets the current wall again is a seam-gap retrace, even
|
||||
// if the rest of the non-extruding wipe remains over printed material.
|
||||
const double contact_tolerance = wipe_tolerance(effective_offset);
|
||||
|
||||
struct Candidate {
|
||||
Polyline path;
|
||||
// Orca: support score chooses the material-side path; length is used
|
||||
// only to replace a corner-truncated path with the reverse fallback.
|
||||
double support_score;
|
||||
double path_length;
|
||||
};
|
||||
|
||||
// Direction and wall contact have different origins after a loop pre-move.
|
||||
// Keep the construction's wall endpoint for intersection checks even when
|
||||
// the candidate's direction must be checked from the current nozzle position.
|
||||
const auto validate_candidate = [&](Polyline path, Point path_start, Point direction_start,
|
||||
double path_contact_tolerance,
|
||||
const Vec2d &candidate_support_direction,
|
||||
double candidate_offset,
|
||||
bool require_clearance = true) -> std::optional<Candidate> {
|
||||
// Orca: backtracking indicates a wrong join only across a nonzero gap.
|
||||
// A closed zero-gap offset may initially turn back at its miter while
|
||||
// still remaining on the supported material side of the perimeter.
|
||||
const bool backtracks_across_gap = seam_start != seam_end && starts_by_backtracking(path, wipe_start);
|
||||
// At a clipped corner another branch of the current wall may be closer
|
||||
// than the requested offset. Preserve the zero-gap clearance rule, but
|
||||
// check direction and local material side independently for every gap.
|
||||
const bool material_side = wipe_path_stays_on_material_side(
|
||||
path, direction_start, candidate_support_direction,
|
||||
support_distancer, current_perimeter_distancer, candidate_offset,
|
||||
require_clearance && seam_start == seam_end);
|
||||
const bool connector_clear = initial_connector_is_clear(
|
||||
path, wipe_start, path_start, current_perimeter_distancer, path_contact_tolerance);
|
||||
if (backtracks_across_gap || ! material_side || ! connector_clear)
|
||||
return std::nullopt;
|
||||
const std::optional<double> score = wipe_path_support_score(
|
||||
path, wipe_start, support_distancer, all_support_distancer, max_support_distance);
|
||||
if (! score)
|
||||
return std::nullopt;
|
||||
const double path_length = executable_path_length(path, wipe_start);
|
||||
return Candidate{std::move(path), *score, path_length};
|
||||
};
|
||||
|
||||
const auto offset_candidate = [&](int dir) -> std::optional<Candidate> {
|
||||
Polyline path = polyline;
|
||||
if (! offset_wipe_path(path, seam_start, seam_end, wipe_start, dir,
|
||||
effective_offset, max_wipe_length))
|
||||
return std::nullopt;
|
||||
return validate_candidate(std::move(path), seam_start, seam_start,
|
||||
contact_tolerance, support_direction, effective_offset);
|
||||
};
|
||||
|
||||
std::optional<Candidate> preferred = offset_candidate(preferred_dir);
|
||||
std::optional<Candidate> alternate = offset_candidate(-preferred_dir);
|
||||
|
||||
// Orca: forward and reverse fallbacks share the same clamping, translation,
|
||||
// connector tolerance, and complete-path validation.
|
||||
const auto translated_candidate = [&](Polyline source, Point source_start, Point source_end,
|
||||
const Vec2d &candidate_support_offset) -> std::optional<Candidate> {
|
||||
const double support_distance = candidate_support_offset.norm();
|
||||
const double candidate_offset = std::min(offset_dist, support_distance);
|
||||
if (candidate_offset <= SCALED_EPSILON)
|
||||
return std::nullopt;
|
||||
|
||||
const Vec2d candidate_translation = candidate_support_offset * (candidate_offset / support_distance);
|
||||
if (! translated_wipe_path(source, source_start, source_end, wipe_start,
|
||||
candidate_translation, max_wipe_length))
|
||||
return std::nullopt;
|
||||
const double candidate_tolerance = wipe_tolerance(candidate_offset);
|
||||
return validate_candidate(std::move(source), source_start, source_start, candidate_tolerance,
|
||||
candidate_support_offset / support_distance, candidate_offset);
|
||||
};
|
||||
|
||||
std::optional<Candidate> translated = translated_candidate(polyline, seam_start, seam_end, toward_support);
|
||||
|
||||
// Orca: if every full-length construction folds back onto the external
|
||||
// wall, retain a short direct inward move instead of accepting an outward
|
||||
// candidate or falling back to the standard wipe along the outer wall.
|
||||
const auto direct_candidate = [&](Point origin, const Vec2d &candidate_support_offset) -> std::optional<Candidate> {
|
||||
const double support_distance = candidate_support_offset.norm();
|
||||
const double candidate_offset = std::min(offset_dist, support_distance);
|
||||
if (candidate_offset <= SCALED_EPSILON)
|
||||
return std::nullopt;
|
||||
const Vec2d direction = candidate_support_offset / support_distance;
|
||||
const Point destination = (origin.cast<double>() + direction * candidate_offset).cast<coord_t>();
|
||||
if (destination == wipe_start)
|
||||
return std::nullopt;
|
||||
|
||||
Polyline path;
|
||||
if (! store_wipe_path(path, seam_start, Polyline{wipe_start, destination}, max_wipe_length))
|
||||
return std::nullopt;
|
||||
const double candidate_tolerance = wipe_tolerance(candidate_offset);
|
||||
// Check the executed direction from the nozzle after any loop pre-move,
|
||||
// but retain the wall origin for the connector's intersection checks.
|
||||
return validate_candidate(std::move(path), origin, wipe_start,
|
||||
candidate_tolerance, direction, candidate_offset, false);
|
||||
};
|
||||
std::optional<Candidate> direct = direct_candidate(seam_end, toward_support);
|
||||
|
||||
const double length_margin = wipe_tolerance(max_wipe_length);
|
||||
std::optional<Candidate> reversed;
|
||||
if (seam_start != seam_end && polyline.last_point() == seam_end) {
|
||||
// Orca: when a large gap straddles a sharp corner, connecting the
|
||||
// extrusion end to the forward offset may either reverse or leave only
|
||||
// a short local move. The already printed incoming wall is equally safe:
|
||||
// follow it backwards and determine its own material-side support.
|
||||
Polyline reversed_source = polyline;
|
||||
reversed_source.reverse();
|
||||
const std::optional<Vec2d> reversed_support_offset = support_offset_at_start(
|
||||
reversed_source, seam_end, true, support_distancer, max_support_distance);
|
||||
if (reversed_support_offset) {
|
||||
reversed = translated_candidate(reversed_source, seam_end, seam_end, *reversed_support_offset);
|
||||
// A translated reverse path can backtrack or leave the material on
|
||||
// a curved wall. Offset the incoming wall itself when translation
|
||||
// cannot supply a complete wipe, retaining all candidate checks.
|
||||
if (! reversed || reversed->path_length + length_margin < max_wipe_length) {
|
||||
const double reverse_offset = std::min(offset_dist, reversed_support_offset->norm());
|
||||
if (reverse_offset > SCALED_EPSILON &&
|
||||
offset_wipe_path(reversed_source, seam_end, seam_start, wipe_start,
|
||||
-preferred_dir, reverse_offset, max_wipe_length)) {
|
||||
reversed_source.points.front() = seam_start;
|
||||
auto candidate = validate_candidate(std::move(reversed_source), seam_end, seam_end,
|
||||
wipe_tolerance(reverse_offset), reversed_support_offset->normalized(), reverse_offset);
|
||||
if (candidate && (! reversed ||
|
||||
(candidate->path_length > reversed->path_length + length_margin &&
|
||||
candidate->support_score <= reversed->support_score + wipe_tolerance(reverse_offset))))
|
||||
reversed = std::move(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Orca: conventional offsets at a narrow cusp may form a bevel across the
|
||||
// cusp. Candidates pointing away from the actual inner wall are rejected
|
||||
// during validation; among the remaining paths, prefer the one whose first
|
||||
// point is materially closer to that wall.
|
||||
const double direction_change_margin = wipe_tolerance(effective_offset);
|
||||
std::optional<Candidate> selected = std::move(preferred);
|
||||
if (translated) {
|
||||
if (! selected || translated->support_score + direction_change_margin < selected->support_score)
|
||||
selected = std::move(translated);
|
||||
}
|
||||
if (! selected)
|
||||
selected = std::move(direct);
|
||||
// Prefer a direct inward move when the normal offset cannot be used.
|
||||
// An alternate offset is eligible only after the same material-side checks.
|
||||
if (! selected)
|
||||
selected = std::move(alternate);
|
||||
|
||||
// Orca: prefer a complete reverse wipe over a forward fallback that had to
|
||||
// stop at the corner. Equal-length paths keep the normal forward behavior.
|
||||
if (reversed && (! selected ||
|
||||
(reversed->path_length > selected->path_length + length_margin &&
|
||||
reversed->support_score <= selected->support_score + direction_change_margin)))
|
||||
selected = std::move(reversed);
|
||||
if (! selected)
|
||||
return false;
|
||||
|
||||
polyline = std::move(selected->path);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<Point> wipe_on_loops_destination(const ExtrusionPaths &paths, double nozzle_diam_scaled,
|
||||
bool is_ccw, bool is_hole)
|
||||
{
|
||||
assert(!paths.empty());
|
||||
assert(nozzle_diam_scaled > 0);
|
||||
if (paths.empty() || nozzle_diam_scaled <= 0)
|
||||
return std::nullopt;
|
||||
|
||||
// Orca: clamp sample distance to L/4 so forward/backward samples cannot meet.
|
||||
double total_length = 0.;
|
||||
for (const ExtrusionPath &path : paths)
|
||||
total_length += path.length();
|
||||
const double sample_distance = std::min(nozzle_diam_scaled, total_length * 0.25);
|
||||
|
||||
Point a = sample_path_at_distance(paths, true, sample_distance);
|
||||
Point b = sample_path_at_distance(paths, false, sample_distance);
|
||||
|
||||
const Point seam_start = paths.front().first_point();
|
||||
|
||||
// Orca: skip the inward move for degenerate geometry.
|
||||
if (a == b || a == seam_start || b == seam_start)
|
||||
return std::nullopt;
|
||||
|
||||
const bool reverse_turn = is_hole == is_ccw;
|
||||
if (reverse_turn)
|
||||
std::swap(a, b);
|
||||
|
||||
double angle = seam_start.ccw_angle(a, b) / 3;
|
||||
|
||||
// Orca: reject degenerate angles near 0 or 2π.
|
||||
static constexpr double angle_epsilon = 0.01;
|
||||
if (angle < angle_epsilon || angle > 2 * PI / 3 - angle_epsilon)
|
||||
return std::nullopt;
|
||||
|
||||
if (reverse_turn)
|
||||
angle *= -1;
|
||||
|
||||
Point pt = sample_path_at_distance(paths, true, std::min(0.2 * nozzle_diam_scaled, sample_distance));
|
||||
pt.rotate(angle, seam_start);
|
||||
return pt;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "../ExtrusionEntity.hpp"
|
||||
#include "../Polyline.hpp"
|
||||
#include "../Line.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Printed prefix of one region's perimeter sequence. Append each entity only
|
||||
// after extrusion; later walls and other regions cannot support an inward wipe.
|
||||
struct WipeInwardSupport {
|
||||
Lines printed_lines;
|
||||
Lines inner_lines;
|
||||
void append(const ExtrusionEntity &entity);
|
||||
};
|
||||
|
||||
namespace AABBTreeLines {
|
||||
template <typename LineType> class LinesDistancer;
|
||||
}
|
||||
|
||||
// Orca: sample a point at a given distance along ExtrusionPaths, walking
|
||||
// across segment boundaries. forward=true walks from paths.front, false from
|
||||
// paths.back. For tiny loops the walk stops early and returns the last
|
||||
// reachable point. Returns the start point if target is zero.
|
||||
// Precondition: paths must be non-empty.
|
||||
Point sample_path_at_distance(const ExtrusionPaths &paths, bool forward, double target);
|
||||
|
||||
// Orca: return the side of the printed path on which the material lies.
|
||||
// dir +1 is left and -1 is right, matching the offset-builder convention.
|
||||
int wipe_offset_direction(bool is_ccw, bool is_hole);
|
||||
|
||||
// Orca: atomically offset a stored wipe path. The seam-gap or closing edge
|
||||
// determines the join with the first outgoing perimeter edge, but its offset
|
||||
// is not part of the executable wipe. Only the prefix needed by Wipe::wipe()
|
||||
// is offset. Returns false and leaves polyline unchanged if that path cannot
|
||||
// be constructed without degenerate segments. This only constructs a candidate;
|
||||
// offset_wipe_path_toward_support() validates its support, material side and
|
||||
// connector before accepting it. The first stored point
|
||||
// remains a dummy preserving Wipe::wipe()'s convention of skipping points[0].
|
||||
// Precondition: polyline starts at seam_start, dir is +1 or -1, and
|
||||
// offset_dist > 0. A non-positive max_wipe_length returns false.
|
||||
bool offset_wipe_path(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||
int dir, double offset_dist, double max_wipe_length);
|
||||
|
||||
// Orca: score a candidate's first destination by distance to the target inner
|
||||
// walls. Return nullopt if no target wall is near wipe_start or any executable
|
||||
// segment lacks support. target_distancer contains eligible earlier walls;
|
||||
// all_support_distancer includes the current wall and all earlier walls.
|
||||
// The stored first point is a dummy: the first segment starts at wipe_start.
|
||||
// This checks support only; material-side and connector checks belong to
|
||||
// offset_wipe_path_toward_support(). Trees are reused across its candidates.
|
||||
std::optional<double> wipe_path_support_score(
|
||||
const Polyline &polyline, Point wipe_start,
|
||||
const AABBTreeLines::LinesDistancer<Line> &target_distancer,
|
||||
const AABBTreeLines::LinesDistancer<Line> &all_support_distancer,
|
||||
double max_distance);
|
||||
|
||||
// Validate the initial inward direction and the local material side along the
|
||||
// executable path, using the inner wall to orient the open current wall's
|
||||
// normals. Clearance is optional for clipped corners and short direct fallbacks;
|
||||
// the material-side check is mandatory. The straight connector is checked by
|
||||
// its initial direction and separately by support and intersection validation.
|
||||
// path_start is the construction origin; points[0] is only a storage sentinel.
|
||||
bool wipe_path_stays_on_material_side(
|
||||
const Polyline &path, Point path_start, const Vec2d &support_direction,
|
||||
const AABBTreeLines::LinesDistancer<Line> &target_perimeter_distancer,
|
||||
const AABBTreeLines::LinesDistancer<Line> ¤t_perimeter_distancer,
|
||||
double effective_offset, bool require_clearance);
|
||||
|
||||
// Orca: identify the adjacent inner perimeter from the outgoing wall, excluding
|
||||
// support on the air side of a closed zero-gap loop. Clamp the requested offset
|
||||
// to the distance from the seam end to that support, then select the safest
|
||||
// supported offset or translated path. If a wide seam gap at a corner truncates
|
||||
// every forward candidate, the incoming printed wall may be followed backwards
|
||||
// instead. All earlier printed perimeters still participate in the complete-path
|
||||
// safety check. This handles converging, locally ambiguous, or self-touching
|
||||
// contours whose global winding alone does not identify the material side.
|
||||
// Returns false and leaves polyline unchanged when no candidate is supported.
|
||||
// Precondition: preferred_dir is +1 or -1. Distances must be positive.
|
||||
bool offset_wipe_path_toward_support(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||
int preferred_dir, double offset_dist, double max_wipe_length,
|
||||
const Lines &target_perimeter_lines, const Lines &printed_perimeter_lines,
|
||||
const Lines ¤t_perimeter_lines,
|
||||
double max_support_distance);
|
||||
|
||||
// Orca: compute the inward destination point for wipe_on_loops, or
|
||||
// std::nullopt when the geometry is degenerate (tiny loop, coincident samples,
|
||||
// angle near 0 or 2π). Returns the rotated destination or nullopt to skip the
|
||||
// inward move entirely.
|
||||
// Precondition: paths non-empty, nozzle_diam_scaled > 0.
|
||||
std::optional<Point> wipe_on_loops_destination(const ExtrusionPaths &paths, double nozzle_diam_scaled,
|
||||
bool is_ccw, bool is_hole);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -25,6 +25,30 @@ static constexpr int arc_fit_size = 20;
|
||||
enum class LimitFlow { None, LimitPrintFlow, LimitRammingFlow, LimitRammingFlowNC};//nc:nozzle change
|
||||
static const std::map<float, float> nozzle_diameter_to_nozzle_change_width{{0.2f, 0.5f}, {0.4f, 1.0f}, {0.6f, 1.2f}, {0.8f, 1.4f}};
|
||||
|
||||
bool wipe_tower_sparse_layers_skipped(const PrintConfig &config)
|
||||
{
|
||||
return config.wipe_tower_no_sparse_layers.value && config.timelapse_type.value != TimelapseType::tlSmooth &&
|
||||
! config.enable_wrapping_detection.value;
|
||||
}
|
||||
|
||||
bool wipe_tower_layer_is_sparse(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes)
|
||||
{
|
||||
return layer_tool_changes.size() == 1 && layer_tool_changes.front().initial_tool == layer_tool_changes.front().new_tool;
|
||||
}
|
||||
|
||||
std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes,
|
||||
float base_z)
|
||||
{
|
||||
std::vector<float> tower_z(tool_changes.size(), base_z);
|
||||
float last = base_z;
|
||||
for (size_t i = 0; i < tool_changes.size(); ++i) {
|
||||
if (! tool_changes[i].empty() && ! wipe_tower_layer_is_sparse(tool_changes[i]))
|
||||
last += tool_changes[i].front().layer_height;
|
||||
tower_z[i] = last;
|
||||
}
|
||||
return tower_z;
|
||||
}
|
||||
|
||||
inline float align_round(float value, float base)
|
||||
{
|
||||
return std::round(value / base) * base;
|
||||
@@ -1349,7 +1373,7 @@ public:
|
||||
// flavor it reaches understands, not the zero dwell the other flavors flush with.
|
||||
buffer += "M400\n";
|
||||
buffer += "M104";
|
||||
if (target_extruder != -1)
|
||||
if (target_extruder != -1 && target_extruder < int(m_physical_extruder_map.size()))
|
||||
buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder]));
|
||||
buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by slicer
|
||||
if (!comment.empty()) buffer += " ;" + comment;
|
||||
@@ -1361,7 +1385,7 @@ public:
|
||||
WipeTowerWriter &format_line_M109(int target_temp, int target_extruder, const std::string &comment = std::string())
|
||||
{
|
||||
std::string buffer = "M109";
|
||||
if (target_extruder != -1)
|
||||
if (target_extruder != -1 && target_extruder < int(m_physical_extruder_map.size()))
|
||||
buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder]));
|
||||
buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by slicer
|
||||
if (!comment.empty()) buffer += " ;" + comment;
|
||||
@@ -1630,6 +1654,94 @@ float WipeTower::get_auto_brim_by_height(float max_height) {
|
||||
return 8.f;
|
||||
}
|
||||
|
||||
float WipeTower::estimate_brim_real_width(float brim_width, float nozzle_diameter, float first_layer_height, bool type2)
|
||||
{
|
||||
if (brim_width <= 0.f)
|
||||
return brim_width;
|
||||
const float spacing = nozzle_diameter * 1.25f - first_layer_height * float(1. - M_PI_4); // Width_To_Nozzle_Ratio
|
||||
if (spacing <= EPSILON)
|
||||
return brim_width;
|
||||
const int loops_num = int((brim_width + spacing / 2.f) / spacing);
|
||||
return loops_num * spacing + (type2 ? 0.f : spacing / 2.f);
|
||||
}
|
||||
|
||||
float WipeTower::get_wrapping_detection_depth()
|
||||
{
|
||||
return float(wrapping_wipe_tower_depth);
|
||||
}
|
||||
|
||||
float WipeTower::nozzle_change_perimeter_width(float nozzle_diameter)
|
||||
{
|
||||
auto it = nozzle_diameter_to_nozzle_change_width.find(nozzle_diameter);
|
||||
return it != nozzle_diameter_to_nozzle_change_width.end() ? it->second : 2.f * nozzle_diameter * 1.25f;
|
||||
}
|
||||
|
||||
float WipeTower::estimate_tower_blocks_depth(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing)
|
||||
{
|
||||
if (purges.empty() || layer_height < EPSILON || nozzle_diameter < EPSILON)
|
||||
return 0.f;
|
||||
const float pw = nozzle_diameter * 1.25f; // Width_To_Nozzle_Ratio
|
||||
const float ncpw = nozzle_change_perimeter_width(nozzle_diameter);
|
||||
const float line_width = width - 2.f * pw;
|
||||
if (line_width <= EPSILON)
|
||||
return 0.f;
|
||||
// Line cross-section as volume_to_length() sees it; the infill gap stretches the perimeter
|
||||
// width by the configured ratio and nozzle-change lines keep their own width
|
||||
// (calc_block_infill_gap).
|
||||
auto line_area = [layer_height](float w) { return layer_height * (w - layer_height * float(1. - M_PI_4)); };
|
||||
const float extra_width = (extra_spacing - 1.f) * pw;
|
||||
const float gap = pw + extra_width;
|
||||
const float nc_gap = ncpw + extra_width;
|
||||
// A layer purges into at most (filaments - 1) targets, so a category holding every filament
|
||||
// never sees its smallest purge (the layer's first filament) in its worst layer.
|
||||
struct Block { float depth = 0.f; float min_purge = 0.f; size_t filaments = 0; };
|
||||
std::map<int, Block> blocks;
|
||||
for (const PurgeEstimate &purge : purges) {
|
||||
Block &block = blocks[purge.category];
|
||||
const float purge_depth = std::ceil(purge.prime_volume / line_area(pw) / line_width) * gap;
|
||||
block.min_purge = block.filaments == 0 ? purge_depth : std::min(block.min_purge, purge_depth);
|
||||
block.depth += purge_depth;
|
||||
++block.filaments;
|
||||
if (purge.filament_change_length > EPSILON) {
|
||||
// The leaving filament is rammed over the nozzle-change flow, again in whole lines.
|
||||
const float filament_area = float(M_PI) * purge.filament_diameter * purge.filament_diameter / 4.f;
|
||||
const float nc_length = purge.filament_change_length * filament_area / line_area(ncpw);
|
||||
block.depth += std::ceil(nc_length / (width - ncpw - pw)) * nc_gap;
|
||||
}
|
||||
}
|
||||
float depth = pw; // plan_tower_new starts the first block one perimeter width in
|
||||
for (const auto &[category, block] : blocks)
|
||||
depth += block.filaments == purges.size() ? block.depth - block.min_purge : block.depth;
|
||||
return depth;
|
||||
}
|
||||
|
||||
float WipeTower::rib_footprint_side(float width, float depth, float rib_width, float extra_rib_length, float max_height)
|
||||
{
|
||||
if (width < EPSILON || depth < EPSILON)
|
||||
return 0.f;
|
||||
// Ribs run the diagonal; below the height-based minimum they are extended rather than the
|
||||
// body, then by the extra length, never ending up shorter than the diagonal.
|
||||
const float diagonal = std::sqrt(width * width + depth * depth);
|
||||
float rib_length = diagonal;
|
||||
if (depth + EPSILON < get_limit_depth_by_height(max_height))
|
||||
rib_length = std::max(rib_length, get_limit_depth_by_height(max_height) * float(std::sqrt(2.)));
|
||||
rib_length = std::max(diagonal, rib_length + extra_rib_length);
|
||||
// Half the extension at each end of the diagonal plus half the rib width, projected onto the axes.
|
||||
const float rib_w = std::min(rib_width, std::min(width, depth) / 2.f);
|
||||
const float per_side = ((rib_length - diagonal) / 2.f + rib_w / 2.f) / float(std::sqrt(2.));
|
||||
return std::max(width, depth) + 2.f * per_side;
|
||||
}
|
||||
|
||||
float WipeTower::estimate_rib_tower_bbox_side(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing, float rib_width, float extra_rib_length, float max_height)
|
||||
{
|
||||
if (purges.empty() || width < EPSILON || layer_height < EPSILON || nozzle_diameter < EPSILON)
|
||||
return 0.f;
|
||||
const float pw = nozzle_diameter * 1.25f; // Width_To_Nozzle_Ratio
|
||||
const float square = align_ceil(std::sqrt(estimate_tower_blocks_depth(purges, width, layer_height, nozzle_diameter, extra_spacing) * width), pw);
|
||||
const float depth = estimate_tower_blocks_depth(purges, square, layer_height, nozzle_diameter, extra_spacing);
|
||||
return rib_footprint_side(square, depth, rib_width, extra_rib_length, max_height);
|
||||
}
|
||||
|
||||
Vec2f WipeTower::move_box_inside_polygon(const BoundingBox &box, const Polygons &polygons, coord_t offset)
|
||||
{
|
||||
if (polygons.empty()) return Vec2f{0.f, 0.f};
|
||||
@@ -1791,7 +1903,7 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi
|
||||
m_z_pos(0.f),
|
||||
//m_bridging(float(config.wipe_tower_bridging)),
|
||||
m_bridging(10.f),
|
||||
m_no_sparse_layers(config.wipe_tower_no_sparse_layers),
|
||||
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
|
||||
m_gcode_flavor(config.gcode_flavor),
|
||||
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
|
||||
m_current_tool(initial_tool),
|
||||
@@ -2889,7 +3001,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer(bool extrude_perimeter, bool
|
||||
|
||||
// Ask our writer about how much material was consumed.
|
||||
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
|
||||
if (! m_no_sparse_layers || toolchanges_on_layer)
|
||||
if (! m_sparse_layers_skipped || toolchanges_on_layer)
|
||||
if (m_current_tool < m_used_filament_length.size())
|
||||
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
|
||||
|
||||
@@ -2933,7 +3045,7 @@ void WipeTower::plan_toolchange(float z_par, float layer_height_par, unsigned in
|
||||
if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) // if we moved to a new layer, we'll add it to m_plan first
|
||||
m_plan.push_back(WipeTowerInfo(z_par, layer_height_par));
|
||||
|
||||
if (m_first_layer_idx == size_t(-1) && (! m_no_sparse_layers || old_tool != new_tool))
|
||||
if (m_first_layer_idx == size_t(-1) && (! m_sparse_layers_skipped || old_tool != new_tool))
|
||||
m_first_layer_idx = m_plan.size() - 1;
|
||||
|
||||
if (old_tool == new_tool) // new layer without toolchanges - we are done
|
||||
@@ -3221,7 +3333,7 @@ void WipeTower::get_wall_skip_points(const WipeTowerInfo &layer, int layer_id)
|
||||
if (!cur_block_depth.count(m_filpar[new_filament].category)) cur_block_depth[m_filpar[new_filament].category] = block->start_depth;
|
||||
process_depth = cur_block_depth[m_filpar[new_filament].category];
|
||||
if (is_need_ramming(new_filament, old_filament, layer_id)) {
|
||||
if (m_filament_categories[new_filament] == m_filament_categories[old_filament])
|
||||
if (get_filament_category(new_filament) == get_filament_category(old_filament))
|
||||
process_depth += nozzle_change_depth;
|
||||
else {
|
||||
if (!cur_block_depth.count(m_filpar[old_filament].category)) {
|
||||
@@ -3786,7 +3898,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer_new(bool extrude_perimeter,
|
||||
|
||||
// Ask our writer about how much material was consumed.
|
||||
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
|
||||
if (!m_no_sparse_layers || toolchanges_on_layer)
|
||||
if (!m_sparse_layers_skipped || toolchanges_on_layer)
|
||||
if (m_current_tool < m_used_filament_length.size())
|
||||
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
|
||||
|
||||
@@ -3896,7 +4008,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block(const WipeTowerBlock &block,
|
||||
|
||||
// Ask our writer about how much material was consumed.
|
||||
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
|
||||
if (!m_no_sparse_layers || toolchanges_on_layer)
|
||||
if (!m_sparse_layers_skipped || toolchanges_on_layer)
|
||||
if (filament_id < m_used_filament_length.size())
|
||||
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
|
||||
|
||||
@@ -4013,7 +4125,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block_solid(const WipeTowerBlock &
|
||||
|
||||
// Ask our writer about how much material was consumed.
|
||||
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
|
||||
if (!m_no_sparse_layers || toolchanges_on_layer)
|
||||
if (!m_sparse_layers_skipped || toolchanges_on_layer)
|
||||
if (filament_id < m_used_filament_length.size())
|
||||
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
|
||||
|
||||
@@ -4695,7 +4807,7 @@ int WipeTower::get_wall_filament_for_all_layer()
|
||||
int filament_id = -1;
|
||||
int filament_count = 0;
|
||||
for (auto iter = filament_counts.begin(); iter != filament_counts.end(); ++iter) {
|
||||
if (m_filament_categories[iter->first] == selected_category && iter->second > filament_count) {
|
||||
if (get_filament_category(iter->first) == selected_category && iter->second > filament_count) {
|
||||
filament_id = iter->first;
|
||||
filament_count = iter->second;
|
||||
}
|
||||
@@ -4883,12 +4995,8 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_inserted) {
|
||||
if (finish_block_tcr.gcode.empty())
|
||||
finish_block_tcr = finish_block_tcr;
|
||||
else
|
||||
finish_layer_tcr = merge_tcr(finish_layer_tcr, finish_block_tcr);
|
||||
}
|
||||
if (!has_inserted && !finish_block_tcr.gcode.empty())
|
||||
finish_layer_tcr = merge_tcr(finish_layer_tcr, finish_block_tcr);
|
||||
}
|
||||
}
|
||||
// record the contact layers of different categories
|
||||
@@ -5071,7 +5179,7 @@ WipeTower::ToolChangeResult WipeTower::only_generate_out_wall(bool is_new_mode)
|
||||
|
||||
// Ask our writer about how much material was consumed.
|
||||
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
|
||||
if (!m_no_sparse_layers || toolchanges_on_layer)
|
||||
if (!m_sparse_layers_skipped || toolchanges_on_layer)
|
||||
if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
|
||||
|
||||
return construct_tcr(writer, false, old_tool, true, false, 0.f, false);
|
||||
|
||||
@@ -42,9 +42,36 @@ public:
|
||||
static const std::map<float, float> min_depth_per_height;
|
||||
static float get_limit_depth_by_height(float max_height);
|
||||
static float get_auto_brim_by_height(float max_height);
|
||||
// Both generators lay the brim in whole loops one line spacing apart, so the printed width
|
||||
// differs from the configured one. WipeTower reports it with half a spacing of line width
|
||||
// added, WipeTower2 reports the loops alone; an estimate has to round like the generator
|
||||
// whose G-code it stands in for.
|
||||
static float estimate_brim_real_width(float brim_width, float nozzle_diameter, float first_layer_height, bool type2);
|
||||
// Depth a Type1 tower reserves once nothing but wrapping detection asks for one.
|
||||
static float get_wrapping_detection_depth();
|
||||
// Line width of the nozzle-change purge lines at this nozzle diameter.
|
||||
static float nozzle_change_perimeter_width(float nozzle_diameter);
|
||||
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);
|
||||
// One filament's share of a Type1 tower layer, as plan_tower_new() reserves it.
|
||||
struct PurgeEstimate
|
||||
{
|
||||
float prime_volume = 0.f; // mm3 wiped after changing to this filament
|
||||
int category = 0; // filament_adhesiveness_category; one purge block per category
|
||||
float filament_change_length = 0.f; // mm of filament rammed when it leaves its nozzle; 0 when no nozzle change is planned
|
||||
float filament_diameter = 1.75f;
|
||||
};
|
||||
// Depth of the Type1 purge stack at the given width (also the rectangle-wall depth): each
|
||||
// purge is whole lines at the block infill gap, one block per adhesiveness category sized by
|
||||
// its worst layer, stacked behind one perimeter width.
|
||||
static float estimate_tower_blocks_depth(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing);
|
||||
// Side of the square bounding a rib-wall tower's first layer, brim excluded: the body plus the
|
||||
// rib bulge, with the ribs extended to the height-based minimum as both generators do.
|
||||
static float rib_footprint_side(float width, float depth, float rib_width, float extra_rib_length, float max_height);
|
||||
// Type1 rib tower: plan_tower_new() squares the tower from the depth at the configured width,
|
||||
// then re-plans the depth at the squared width.
|
||||
static float estimate_rib_tower_bbox_side(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing, float rib_width, float extra_rib_length, float max_height);
|
||||
// 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
|
||||
@@ -494,7 +521,7 @@ private:
|
||||
//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_sparse_layers_skipped = false;
|
||||
// BBS: remove useless config
|
||||
//bool m_set_extruder_trimpot = false;
|
||||
bool m_adhesion = true;
|
||||
@@ -653,6 +680,24 @@ private:
|
||||
};
|
||||
|
||||
|
||||
// Compaction rule for wipe_tower_no_sparse_layers. Shared by the G-code emitter and by the
|
||||
// clearance validator so that both agree on where the compacted tower actually sits; a drift
|
||||
// between the two would either let a real nozzle collision through or reject a safe plate.
|
||||
|
||||
// Whether sparse layers are really skipped, i.e. whether the tower is compacted at all. Smooth
|
||||
// timelapse and wrapping detection put a tower on every layer, so no layer is ever dropped and the
|
||||
// tower keeps following the object even though the option is on. Tower planning, G-code emission and
|
||||
// the clearance validator all ask this single question, so none of them can compact on its own.
|
||||
bool wipe_tower_sparse_layers_skipped(const PrintConfig &config);
|
||||
|
||||
// A planned layer prints no tower at all when its only toolchange keeps the same filament.
|
||||
bool wipe_tower_layer_is_sparse(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes);
|
||||
|
||||
// Print z the compacted tower reaches on every planned layer. Sparse layers carry over the
|
||||
// previous value, so the tower falls one layer height behind the object for each of them. base_z is
|
||||
// the z the tower starts from, which Orca offsets by z_offset.
|
||||
std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes,
|
||||
float base_z = 0.f);
|
||||
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -1032,7 +1032,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
|
||||
m_y_shift(0.f),
|
||||
m_z_pos(0.f),
|
||||
m_bridging(float(config.wipe_tower_bridging)),
|
||||
m_no_sparse_layers(config.wipe_tower_no_sparse_layers),
|
||||
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
|
||||
m_gcode_flavor(config.gcode_flavor),
|
||||
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
|
||||
m_infill_speed(default_region_config.sparse_infill_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
|
||||
@@ -1730,7 +1730,7 @@ void WipeTower2::toolchange_Change(
|
||||
} else if (m_wall_type == (int)wtwCone) {
|
||||
const double support_scale = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth,
|
||||
m_wipe_tower_cone_angle).second;
|
||||
const double z = m_no_sparse_layers ? (m_current_height + m_layer_info->height) : m_layer_info->z;
|
||||
const double z = m_sparse_layers_skipped ? (m_current_height + m_layer_info->height) : m_layer_info->z;
|
||||
const double r = std::tan(Geometry::deg2rad(m_wipe_tower_cone_angle / 2.f)) * (m_wipe_tower_height - z);
|
||||
const double w = m_layer_info->depth + m_perimeter_width;
|
||||
if (r > 0.5 * w + 0.01) { // same guard as generate_support_cone_wall
|
||||
@@ -1872,7 +1872,7 @@ void WipeTower2::toolchange_Wipe(
|
||||
// All the calculations in all other places take the spacing into account for all the layers.
|
||||
|
||||
// If spare layers are excluded->if 1 or less toolchange has been done, it must be sill the first layer, too.So slow down.
|
||||
const float target_speed = is_first_layer() || (m_num_tool_changes <= 1 && m_no_sparse_layers) ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
|
||||
const float target_speed = is_first_layer() || (m_num_tool_changes <= 1 && m_sparse_layers_skipped) ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
|
||||
float wipe_speed = 0.33f * target_speed;
|
||||
|
||||
// if there is less than 2.5*line_width to the edge, advance straightaway (there is likely a blob anyway)
|
||||
@@ -1970,7 +1970,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
|
||||
|
||||
// Slow down on the 1st layer.
|
||||
// If spare layers are excluded -> if 1 or less toolchange has been done, it must be still the first layer, too. So slow down.
|
||||
bool first_layer = is_first_layer() || (m_num_tool_changes <= 1 && m_no_sparse_layers);
|
||||
bool first_layer = is_first_layer() || (m_num_tool_changes <= 1 && m_sparse_layers_skipped);
|
||||
float feedrate = first_layer ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
|
||||
if (m_enable_tower_interface_features && m_prev_layer_had_interface)
|
||||
feedrate = std::min(feedrate, 20.f * 60.f);
|
||||
@@ -2103,7 +2103,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
|
||||
|
||||
// Ask our writer about how much material was consumed.
|
||||
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
|
||||
if (! m_no_sparse_layers || toolchanges_on_layer || first_layer) {
|
||||
if (! m_sparse_layers_skipped || toolchanges_on_layer || first_layer) {
|
||||
if (m_current_tool < m_used_filament_length.size())
|
||||
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
|
||||
m_current_height += m_layer_info->height;
|
||||
@@ -2129,6 +2129,23 @@ std::pair<double, double> WipeTower2::get_wipe_tower_cone_base(double width, dou
|
||||
return std::make_pair(R, support_scale);
|
||||
}
|
||||
|
||||
Polygon WipeTower2::cone_base_polygon(double width, double depth, double height, double angle_deg)
|
||||
{
|
||||
Polygon box({Point::new_scale(Vec2d(0., 0.)), Point::new_scale(Vec2d(width, 0.)),
|
||||
Point::new_scale(Vec2d(width, depth)), Point::new_scale(Vec2d(0., depth))});
|
||||
if (angle_deg <= EPSILON || height <= EPSILON || width <= EPSILON || depth <= EPSILON)
|
||||
return box;
|
||||
const auto [R, x_scale] = get_wipe_tower_cone_base(width, height, depth, angle_deg);
|
||||
if (R <= EPSILON)
|
||||
return box;
|
||||
const Vec2d center(width / 2., depth / 2.);
|
||||
Polygon ellipse;
|
||||
for (double alpha = 0.; alpha < 2. * M_PI; alpha += M_PI / 20.)
|
||||
ellipse.points.push_back(Point::new_scale(center + R * Vec2d(std::cos(alpha) / x_scale, std::sin(alpha))));
|
||||
Polygons u = union_({box, ellipse});
|
||||
return u.empty() ? box : u.front();
|
||||
}
|
||||
|
||||
// Static method to extract wipe_volumes[from][to] from the configuration.
|
||||
// Takes a ConfigBase so the GUI's wipe tower size estimate can pass the plate's
|
||||
// DynamicPrintConfig directly instead of materializing a full PrintConfig per call.
|
||||
@@ -2209,7 +2226,7 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i
|
||||
if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) // if we moved to a new layer, we'll add it to m_plan first
|
||||
m_plan.push_back(WipeTowerInfo(z_par, layer_height_par));
|
||||
|
||||
if (m_first_layer_idx == size_t(-1) && (! m_no_sparse_layers || old_tool != new_tool || m_plan.size() == 1))
|
||||
if (m_first_layer_idx == size_t(-1) && (! m_sparse_layers_skipped || old_tool != new_tool || m_plan.size() == 1))
|
||||
m_first_layer_idx = m_plan.size() - 1;
|
||||
|
||||
if (old_tool == new_tool) // new layer without toolchanges - we are done
|
||||
@@ -2635,7 +2652,7 @@ Polygon WipeTower2::generate_support_cone_wall(
|
||||
const auto [R, support_scale] = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth,
|
||||
m_wipe_tower_cone_angle);
|
||||
|
||||
double z = m_no_sparse_layers ?
|
||||
double z = m_sparse_layers_skipped ?
|
||||
(m_current_height + m_layer_info->height) :
|
||||
m_layer_info->z; // the former should actually work in both cases, but let's stay on the safe side (the 2.6.0 is close)
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ public:
|
||||
// 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);
|
||||
// First-layer outline of a cone-wall tower in tower-local (scaled) coordinates: body box
|
||||
// unioned with the cone's base ellipse — the model first_layer_wipe_tower_corners uses,
|
||||
// and generate_support_cone_wall stays within it. Brim not included.
|
||||
static Polygon cone_base_polygon(double width, double depth, double height, double angle_deg);
|
||||
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.
|
||||
@@ -263,7 +267,7 @@ private:
|
||||
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_sparse_layers_skipped = false;
|
||||
bool m_set_extruder_trimpot = false;
|
||||
bool m_adhesion = true;
|
||||
GCodeFlavor m_gcode_flavor;
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
#include "WipeTowerEstimate.hpp"
|
||||
|
||||
#include "WipeTower.hpp"
|
||||
#include "WipeTower2.hpp"
|
||||
#include "../Config.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
#include "../libslic3r.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Every caller today declares all these keys, but the signature accepts any ConfigBase: fall
|
||||
// back to the key's declared default, never to a hand-copied constant.
|
||||
static const ConfigOption *option_of(const ConfigBase &config, const char *key)
|
||||
{
|
||||
if (const ConfigOption *opt = config.option(key); opt != nullptr)
|
||||
return opt;
|
||||
if (const ConfigDef *def = config.def(); def != nullptr)
|
||||
if (const ConfigOptionDef *opt_def = def->get(key); opt_def != nullptr)
|
||||
return opt_def->default_value.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
WipeTowerType resolve_wipe_tower_type(const ConfigBase &config)
|
||||
{
|
||||
// printer_model is what the CLI keys its Bambu Lab detection on; the GUI's vendor flag
|
||||
// agrees for every shipped profile.
|
||||
if (const auto *model = dynamic_cast<const ConfigOptionString *>(config.option("printer_model"));
|
||||
model != nullptr && model->value.compare(0, 9, "Bambu Lab") == 0)
|
||||
return WipeTowerType::Type1;
|
||||
// By value, not by concrete type: a static PrintConfig holds ConfigOptionEnum<T>, a
|
||||
// DynamicConfig built from presets holds ConfigOptionEnumGeneric, and both answer getInt().
|
||||
const ConfigOption *type = option_of(config, "wipe_tower_type");
|
||||
return type != nullptr ? WipeTowerType(type->getInt()) : WipeTowerType::Type2;
|
||||
}
|
||||
|
||||
Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTowerType tower_type, double width, double depth, double height)
|
||||
{
|
||||
// Type1 ignores the cone option. The wall type is read by value: a preset-shaped config
|
||||
// holds it as ConfigOptionEnumGeneric, which a cast to ConfigOptionEnum<T> cannot see.
|
||||
const ConfigOption *wall_type = option_of(config, "wipe_tower_wall_type");
|
||||
const ConfigOption *cone_angle = option_of(config, "wipe_tower_cone_angle");
|
||||
const bool cone = tower_type == WipeTowerType::Type2 && wall_type != nullptr &&
|
||||
wall_type->getInt() == int(WipeTowerWallType::wtwCone) && cone_angle != nullptr;
|
||||
return WipeTower2::cone_base_polygon(width, depth, height, cone ? cone_angle->getFloat() : 0.);
|
||||
}
|
||||
|
||||
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeTowerType tower_type, const std::vector<unsigned int> &filament_ids, double layer_height, double max_object_height)
|
||||
{
|
||||
WipeTowerFootprint footprint;
|
||||
footprint.height = max_object_height;
|
||||
const size_t filaments_cnt = filament_ids.size();
|
||||
if (filaments_cnt == 0 || layer_height < EPSILON)
|
||||
return footprint;
|
||||
|
||||
auto opt_float = [&config](const char *key) {
|
||||
const ConfigOption *opt = option_of(config, key);
|
||||
return opt != nullptr ? opt->getFloat() : 0.;
|
||||
};
|
||||
auto opt_bool = [&config](const char *key) {
|
||||
const ConfigOption *opt = option_of(config, key);
|
||||
return opt != nullptr && opt->getBool();
|
||||
};
|
||||
auto opt_enum = [&config](const char *key, int fallback) {
|
||||
const ConfigOption *opt = option_of(config, key);
|
||||
return opt != nullptr ? opt->getInt() : fallback;
|
||||
};
|
||||
auto floats_of = [&config](const char *key) { return dynamic_cast<const ConfigOptionFloats *>(option_of(config, key)); };
|
||||
auto max_of = [&floats_of](const char *key, double fallback) {
|
||||
const auto *opt = floats_of(key);
|
||||
return (opt != nullptr && !opt->values.empty()) ? *std::max_element(opt->values.begin(), opt->values.end()) : fallback;
|
||||
};
|
||||
auto float_at = [&floats_of](const char *key, unsigned int id, double fallback) {
|
||||
const auto *opt = floats_of(key);
|
||||
return (opt != nullptr && !opt->values.empty()) ? opt->get_at(id) : fallback;
|
||||
};
|
||||
auto int_at = [&config](const char *key, unsigned int id, int fallback) {
|
||||
const auto *opt = dynamic_cast<const ConfigOptionInts *>(option_of(config, key));
|
||||
return (opt != nullptr && !opt->values.empty()) ? opt->get_at(id) : fallback;
|
||||
};
|
||||
|
||||
// Both planners size every layer, so the tower has to fit its thinnest one: the first layer
|
||||
// when it is printed thinner than the rest.
|
||||
const double first_layer_height = opt_float("initial_layer_print_height");
|
||||
if (first_layer_height > EPSILON)
|
||||
layer_height = std::min(layer_height, first_layer_height);
|
||||
|
||||
const bool type1 = tower_type == WipeTowerType::Type1;
|
||||
const double width = opt_float("prime_tower_width");
|
||||
const double prime_volume = opt_float("prime_volume");
|
||||
// Type1 spaces its purge lines by prime_tower_infill_gap, Type2 by wipe_tower_extra_spacing.
|
||||
// Type2's extra flow cancels out of the depth: the line length is divided by it and the row
|
||||
// pitch multiplied by it (WipeTower2::get_wipe_depth).
|
||||
const double extra_spacing = opt_float(type1 ? "prime_tower_infill_gap" : "wipe_tower_extra_spacing") / 100.;
|
||||
const double rib_width = opt_float("wipe_tower_rib_width");
|
||||
const double extra_rib_length = opt_float("wipe_tower_extra_rib_length");
|
||||
const auto *nozzle_opt = floats_of("nozzle_diameter");
|
||||
const double nozzle_diameter = (nozzle_opt != nullptr && !nozzle_opt->values.empty()) ? nozzle_opt->values.front() : 0.4;
|
||||
const bool dual_nozzle = nozzle_opt != nullptr && nozzle_opt->values.size() == 2;
|
||||
const bool rib_wall = opt_enum("wipe_tower_wall_type", int(WipeTowerWallType::wtwRectangle)) == int(WipeTowerWallType::wtwRib);
|
||||
const bool smooth_timelapse = opt_enum("timelapse_type", int(TimelapseType::tlTraditional)) == int(TimelapseType::tlSmooth);
|
||||
const bool wrapping = opt_bool("enable_wrapping_detection");
|
||||
// Reasons a tower is printed with no tool change to purge for: the ones that stop
|
||||
// normalize_fdm_2 clearing enable_prime_tower. Its mixed-filament case is not modelled.
|
||||
const bool need_wipe_tower = smooth_timelapse || wrapping;
|
||||
|
||||
// A tower printed for one of the reasons above has no tool change to purge for; both
|
||||
// planners give it the idle depth below and nothing more.
|
||||
const size_t purge_count = filaments_cnt > 1 ? (dual_nozzle ? filaments_cnt : filaments_cnt - 1) : 0;
|
||||
|
||||
// Type2 purges one volume per tool change. Type1 plans per filament below; here the volume
|
||||
// only decides whether a tower exists.
|
||||
double volume = prime_volume * double(purge_count);
|
||||
if (dual_nozzle) {
|
||||
// Dual-nozzle printers also purge the filament change length on the tower.
|
||||
const double length = max_of("filament_change_length", 0.);
|
||||
const double diameter = max_of("filament_diameter", 1.75);
|
||||
volume += length * PI * diameter * diameter / 4. * double(filaments_cnt / 2);
|
||||
}
|
||||
// Single-extruder multi-material purges the flush matrix instead of the prime volume.
|
||||
const bool semm_flush = opt_bool("purge_in_prime_tower") && opt_bool("single_extruder_multi_material");
|
||||
if (semm_flush)
|
||||
volume = WipeTower2::estimate_semm_flush_volume(config, filaments_cnt);
|
||||
|
||||
// The Type1 planner wipes each filament's own prime volume after changing to it, in a block
|
||||
// per adhesiveness category. On a two-nozzle printer the leaving filament is also rammed at
|
||||
// every nozzle change; the tool order groups filaments by nozzle, so a layer crosses
|
||||
// (nozzles used - 1) times, charged here to the longest ramming.
|
||||
std::vector<WipeTower::PurgeEstimate> purges;
|
||||
if (type1 && filaments_cnt > 1) {
|
||||
const bool saving_mode = opt_enum("prime_volume_mode", int(PrimeVolumeMode::pvmDefault)) == int(PrimeVolumeMode::pvmSaving);
|
||||
std::set<int> nozzles;
|
||||
size_t longest_ramming = 0;
|
||||
for (size_t i = 0; i < filaments_cnt; ++i) {
|
||||
const unsigned int id = filament_ids[i];
|
||||
WipeTower::PurgeEstimate purge;
|
||||
purge.prime_volume = saving_mode ? 15.f : float(float_at("filament_prime_volume", id, prime_volume));
|
||||
purge.category = int_at("filament_adhesiveness_category", id, 0);
|
||||
purge.filament_diameter = float(float_at("filament_diameter", id, 1.75));
|
||||
purges.push_back(purge);
|
||||
if (dual_nozzle) {
|
||||
nozzles.insert(int_at("filament_map", id, 1));
|
||||
if (float_at("filament_change_length", id, 0.) > float_at("filament_change_length", filament_ids[longest_ramming], 0.))
|
||||
longest_ramming = i;
|
||||
}
|
||||
}
|
||||
if (nozzles.size() > 1)
|
||||
purges[longest_ramming].filament_change_length = float(float_at("filament_change_length", filament_ids[longest_ramming], 0.) * double(nozzles.size() - 1));
|
||||
}
|
||||
|
||||
// Both wall types decide this together: over-reserving only wastes bed area, but reporting
|
||||
// no tower for one that is built collapses the validation hull to a point.
|
||||
// A tool change is a reason on its own (see the base commit); Type1 already reserves
|
||||
// per filament, Type2 has only the volume, which can resolve to zero.
|
||||
const bool has_purge = type1 ? !purges.empty() : volume > EPSILON;
|
||||
if (!has_purge && filaments_cnt < 2 && !need_wipe_tower)
|
||||
return footprint;
|
||||
|
||||
const double min_depth = WipeTower::get_limit_depth_by_height(float(max_object_height));
|
||||
const float perimeter_width = float(nozzle_diameter) * 1.25f; // Width_To_Nozzle_Ratio
|
||||
// With nothing to purge, plan_tower_new sizes the tower for wrapping detection or the
|
||||
// stability minimum; WipeTower2 only knows the latter.
|
||||
const double idle_depth = (type1 && wrapping && !smooth_timelapse) ? WipeTower::get_wrapping_detection_depth() : min_depth;
|
||||
if (rib_wall) {
|
||||
// Both planners square the tower to the purge area and extend the ribs, not the body,
|
||||
// below the stability minimum.
|
||||
double side;
|
||||
if (!purges.empty())
|
||||
side = WipeTower::estimate_rib_tower_bbox_side(purges, float(width), float(layer_height), float(nozzle_diameter), float(extra_spacing), float(rib_width), float(extra_rib_length), float(max_object_height));
|
||||
else {
|
||||
const double square = has_purge ? std::sqrt(volume / layer_height * extra_spacing) : idle_depth;
|
||||
side = WipeTower::rib_footprint_side(float(square), float(square), float(rib_width), float(extra_rib_length), float(max_object_height));
|
||||
}
|
||||
footprint.width = footprint.depth = side;
|
||||
} else {
|
||||
double depth;
|
||||
if (type1) {
|
||||
// plan_tower_new stretches a short purge stack to the stability minimum behind its
|
||||
// leading perimeter width.
|
||||
depth = purges.empty() ? idle_depth : std::max(min_depth + perimeter_width, double(WipeTower::estimate_tower_blocks_depth(purges, float(width), float(layer_height), float(nozzle_diameter), float(extra_spacing))));
|
||||
} else {
|
||||
depth = volume / (layer_height * width);
|
||||
// The flush volumes already hold the spacing between wipes.
|
||||
if (!semm_flush)
|
||||
depth *= extra_spacing;
|
||||
depth = std::max(min_depth, depth);
|
||||
}
|
||||
footprint.width = width;
|
||||
footprint.depth = depth;
|
||||
}
|
||||
|
||||
footprint.brim_width = opt_float("prime_tower_brim_width");
|
||||
if (footprint.brim_width < 0)
|
||||
footprint.brim_width = WipeTower::get_auto_brim_by_height(float(max_object_height));
|
||||
footprint.brim_width = WipeTower::estimate_brim_real_width(float(footprint.brim_width), float(nozzle_diameter), float(first_layer_height > EPSILON ? first_layer_height : layer_height), !type1);
|
||||
return footprint;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "../Polygon.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class ConfigBase;
|
||||
enum class WipeTowerType;
|
||||
|
||||
// Pre-slice footprint of the wipe tower, shared by validation (Print), the GUI's placement
|
||||
// clamp/preview/arrange and the CLI placement. The arithmetic is shared; the inputs below are
|
||||
// not, so a change to how one caller derives them has to be mirrored in the others.
|
||||
struct WipeTowerFootprint
|
||||
{
|
||||
double width = 0.; // effective width: equals depth for a rib wall, which squares the tower
|
||||
double depth = 0.; // 0 when these inputs imply no tower
|
||||
double height = 0.; // tallest object; drives the stability floor and the auto brim
|
||||
double brim_width = 0.; // printed width: auto (-1) resolved by height, laid in whole loops
|
||||
};
|
||||
|
||||
// Which planner builds the tower: Bambu Lab printers always get Type1, the rest follow
|
||||
// wipe_tower_type. The rule Print::wipe_tower_type() and the CLI apply, read off the config so
|
||||
// the GUI and CLI placement can resolve it without a Print.
|
||||
WipeTowerType resolve_wipe_tower_type(const ConfigBase &config);
|
||||
|
||||
// First-layer outline of an estimated tower in tower-local scaled coordinates, brim excluded:
|
||||
// the body box, or for a Type2 cone wall the box unioned with the cone's base. The preview,
|
||||
// the placement margin and validation all take the outline from here so they cannot disagree
|
||||
// about whether a cone exists.
|
||||
Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTowerType tower_type, double width, double depth, double height);
|
||||
|
||||
// filament_ids: 0-based filaments purged on the plate. The config cannot see custom G-code tool
|
||||
// changes, so ids derived from the model must include them
|
||||
// (Print::extruders(true)) or a real tower is sized as if it were never built.
|
||||
// layer_height: thinnest layer the objects are sliced at. The first layer is folded in here.
|
||||
//
|
||||
// A raft is deliberately not a reason: normalize_fdm_2 clears enable_prime_tower for a plate
|
||||
// purging one filament unless smooth timelapse or wrapping detection is on.
|
||||
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config,
|
||||
WipeTowerType tower_type,
|
||||
const std::vector<unsigned int> &filament_ids,
|
||||
double layer_height,
|
||||
double max_object_height);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -57,7 +57,7 @@
|
||||
#define HAS_INTRINSIC_128_TYPE
|
||||
#endif
|
||||
|
||||
#if defined(_MSC_VER) && defined(_WIN64)
|
||||
#if defined(_MSC_VER) && defined(_M_X64)
|
||||
#include <intrin.h>
|
||||
#pragma intrinsic(_mul128)
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
#include "LayOnFace.hpp"
|
||||
|
||||
#include "Geometry.hpp"
|
||||
#include "Geometry/ConvexHull.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "TriangleMesh.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
std::vector<LayOnFacePlane> lay_on_face_planes(const ModelObject &object, const Transform3d &inst_matrix)
|
||||
{
|
||||
// An object can only rest on its convex hull, so candidate faces are taken from the hull of all model parts.
|
||||
TriangleMesh ch;
|
||||
for (const ModelVolume* vol : object.volumes) {
|
||||
if (vol->type() != ModelVolumeType::MODEL_PART)
|
||||
continue;
|
||||
TriangleMesh vol_ch = vol->get_convex_hull();
|
||||
vol_ch.transform(vol->get_matrix());
|
||||
ch.merge(vol_ch);
|
||||
}
|
||||
ch = ch.convex_hull_3d();
|
||||
std::vector<LayOnFacePlane> planes;
|
||||
|
||||
// Following constants are used for discarding too small polygons.
|
||||
const float minimal_area = 5.f; // in square mm (world coordinates)
|
||||
const float minimal_side = 1.f; // mm
|
||||
const float minimal_angle = 1.f; // degree, initial value was 10, but cause bugs
|
||||
|
||||
// Now we'll go through all the facets and append Points of facets sharing the same normal.
|
||||
// This part is still performed in mesh coordinate system.
|
||||
const int num_of_facets = ch.facets_count();
|
||||
const std::vector<Vec3f> face_normals = its_face_normals(ch.its);
|
||||
const std::vector<Vec3i32> face_neighbors = its_face_neighbors(ch.its);
|
||||
std::vector<int> facet_queue(num_of_facets, 0);
|
||||
std::vector<bool> facet_visited(num_of_facets, false);
|
||||
int facet_queue_cnt = 0;
|
||||
const stl_normal* normal_ptr = nullptr;
|
||||
int facet_idx = 0;
|
||||
while (1) {
|
||||
// Find next unvisited triangle:
|
||||
for (; facet_idx < num_of_facets; ++ facet_idx)
|
||||
if (!facet_visited[facet_idx]) {
|
||||
facet_queue[facet_queue_cnt ++] = facet_idx;
|
||||
facet_visited[facet_idx] = true;
|
||||
normal_ptr = &face_normals[facet_idx];
|
||||
planes.emplace_back();
|
||||
break;
|
||||
}
|
||||
if (facet_idx == num_of_facets)
|
||||
break; // Everything was visited already
|
||||
|
||||
while (facet_queue_cnt > 0) {
|
||||
int facet_idx = facet_queue[-- facet_queue_cnt];
|
||||
const stl_normal& this_normal = face_normals[facet_idx];
|
||||
if (std::abs(this_normal(0) - (*normal_ptr)(0)) < 0.001 && std::abs(this_normal(1) - (*normal_ptr)(1)) < 0.001 && std::abs(this_normal(2) - (*normal_ptr)(2)) < 0.001) {
|
||||
const Vec3i32 face = ch.its.indices[facet_idx];
|
||||
for (int j=0; j<3; ++j)
|
||||
planes.back().outline.emplace_back(ch.its.vertices[face[j]].cast<double>());
|
||||
|
||||
facet_visited[facet_idx] = true;
|
||||
for (int j = 0; j < 3; ++ j)
|
||||
if (int neighbor_idx = face_neighbors[facet_idx][j]; neighbor_idx >= 0 && ! facet_visited[neighbor_idx])
|
||||
facet_queue[facet_queue_cnt ++] = neighbor_idx;
|
||||
}
|
||||
}
|
||||
planes.back().normal = normal_ptr->cast<double>();
|
||||
|
||||
Pointf3s& verts = planes.back().outline;
|
||||
// Now we'll transform all the points into world coordinates, so that the areas, angles and distances
|
||||
// make real sense.
|
||||
verts = transform(verts, inst_matrix);
|
||||
|
||||
// if this is a just a very small triangle, remove it to speed up further calculations (it would be rejected later anyway):
|
||||
if (verts.size() == 3 &&
|
||||
((verts[0] - verts[1]).norm() < minimal_side
|
||||
|| (verts[0] - verts[2]).norm() < minimal_side
|
||||
|| (verts[1] - verts[2]).norm() < minimal_side))
|
||||
planes.pop_back();
|
||||
}
|
||||
|
||||
// Let's prepare transformation of the normal vector from mesh to instance coordinates.
|
||||
const Matrix3d normal_matrix = inst_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
|
||||
|
||||
// Now we'll go through all the polygons, transform the points into xy plane to process them:
|
||||
for (unsigned int polygon_id=0; polygon_id < planes.size(); ++polygon_id) {
|
||||
Pointf3s& polygon = planes[polygon_id].outline;
|
||||
const Vec3d& normal = planes[polygon_id].normal;
|
||||
|
||||
// transform the normal according to the instance matrix:
|
||||
const Vec3d normal_transformed = normal_matrix * normal;
|
||||
|
||||
// We are going to rotate about z and y to flatten the plane
|
||||
Eigen::Quaterniond q;
|
||||
Transform3d& m = planes[polygon_id].to_plane_frame;
|
||||
m = Transform3d::Identity();
|
||||
m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(normal_transformed, Vec3d::UnitZ()).toRotationMatrix();
|
||||
polygon = transform(polygon, m);
|
||||
|
||||
// Now to remove the inner points. We'll misuse Geometry::convex_hull for that, but since
|
||||
// it works in fixed point representation, we will rescale the polygon to avoid overflows.
|
||||
// And yes, it is a nasty thing to do. Whoever has time is free to refactor.
|
||||
Vec3d bb_size = BoundingBoxf3(polygon).size();
|
||||
float sf = std::min(1./bb_size(0), 1./bb_size(1));
|
||||
Transform3d tr = Geometry::scale_transform({ sf, sf, 1.f });
|
||||
polygon = transform(polygon, tr);
|
||||
polygon = Slic3r::Geometry::convex_hull(polygon);
|
||||
polygon = transform(polygon, tr.inverse());
|
||||
|
||||
// Calculate area of the polygons and discard ones that are too small
|
||||
float& area = planes[polygon_id].area;
|
||||
area = 0.f;
|
||||
for (unsigned int i = 0; i < polygon.size(); i++) // Shoelace formula
|
||||
area += polygon[i](0)*polygon[i + 1 < polygon.size() ? i + 1 : 0](1) - polygon[i + 1 < polygon.size() ? i + 1 : 0](0)*polygon[i](1);
|
||||
area = 0.5f * std::abs(area);
|
||||
|
||||
bool discard = false;
|
||||
if (area < minimal_area)
|
||||
discard = true;
|
||||
else {
|
||||
// We also check the inner angles and discard polygons with angles smaller than the following threshold
|
||||
const double angle_threshold = ::cos(minimal_angle * (double)PI / 180.0);
|
||||
|
||||
for (unsigned int i = 0; i < polygon.size(); ++i) {
|
||||
const Vec3d& prec = polygon[(i == 0) ? polygon.size() - 1 : i - 1];
|
||||
const Vec3d& curr = polygon[i];
|
||||
const Vec3d& next = polygon[(i == polygon.size() - 1) ? 0 : i + 1];
|
||||
|
||||
if ((prec - curr).normalized().dot((next - curr).normalized()) > angle_threshold) {
|
||||
discard = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (discard) {
|
||||
planes[polygon_id--] = std::move(planes.back());
|
||||
planes.pop_back();
|
||||
continue;
|
||||
}
|
||||
|
||||
const Vec3d centroid = std::accumulate(polygon.begin(), polygon.end(), Vec3d(0.0, 0.0, 0.0)) / double(polygon.size());
|
||||
planes[polygon_id].center = inst_matrix.inverse() * (m.inverse() * centroid);
|
||||
}
|
||||
|
||||
std::sort(planes.rbegin(), planes.rend(), [](const LayOnFacePlane& a, const LayOnFacePlane& b) { return a.area < b.area; });
|
||||
return planes;
|
||||
}
|
||||
|
||||
int find_largest_plane(const std::vector<LayOnFacePlane> &planes)
|
||||
{
|
||||
// The plane frame maps the instance normal to +Z, so the normal's z in instance coordinates is element (2, 2).
|
||||
auto downward = [](const LayOnFacePlane &plane) { return -plane.to_plane_frame.linear()(2, 2); };
|
||||
// Areas are floats from rounded geometry, so faces within 0.1% count as equal.
|
||||
int best = -1;
|
||||
for (size_t i = 0; i < planes.size() && planes[i].area >= planes.front().area * (1. - 1e-3); ++i)
|
||||
if (best < 0 || downward(planes[i]) > downward(planes[best]))
|
||||
best = int(i);
|
||||
return best;
|
||||
}
|
||||
|
||||
int find_plane_by_normal(const std::vector<LayOnFacePlane> &planes, const Vec3d &direction)
|
||||
{
|
||||
const Vec3d dir = direction.normalized();
|
||||
int best = -1;
|
||||
double best_dot = -2.;
|
||||
for (size_t i = 0; i < planes.size(); ++i)
|
||||
if (const double dot = planes[i].normal.dot(dir); dot > best_dot) {
|
||||
best_dot = dot;
|
||||
best = int(i);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transform3d &instance_matrix_no_offset,
|
||||
const Vec3d &point, double tolerance)
|
||||
{
|
||||
const Vec3d instance_point = instance_matrix_no_offset * point;
|
||||
for (size_t i = 0; i < planes.size(); ++i) {
|
||||
const Pointf3s &outline = planes[i].outline;
|
||||
if (outline.empty())
|
||||
continue;
|
||||
const Vec3d p = planes[i].to_plane_frame * instance_point;
|
||||
// Facets with slightly different normals are merged into one face, so the outline is not exactly flat.
|
||||
const double z = std::accumulate(outline.begin(), outline.end(), 0., [](double sum, const Vec3d &v) { return sum + v.z(); }) / double(outline.size());
|
||||
if (std::abs(p.z() - z) > tolerance)
|
||||
continue;
|
||||
// The outline is convex: the point is inside when it is not on both sides of its edges.
|
||||
bool left = false, right = false;
|
||||
for (size_t j = 0; j < outline.size(); ++j) {
|
||||
const Vec2d a = outline[j].head<2>();
|
||||
const Vec2d edge = outline[(j + 1) % outline.size()].head<2>() - a;
|
||||
const double len = edge.norm();
|
||||
if (len < EPSILON)
|
||||
continue;
|
||||
const double side = cross2(edge, Vec2d(p.head<2>() - a)) / len;
|
||||
left |= side > tolerance;
|
||||
right |= side < -tolerance;
|
||||
}
|
||||
if (!(left && right))
|
||||
return int(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal)
|
||||
{
|
||||
ModelInstance &instance = *object.instances[instance_idx];
|
||||
const Geometry::Transformation &trafo = instance.get_transformation();
|
||||
// Same rotation as Selection::flattening_rotate(): turn the transformed normal to point down.
|
||||
const Vec3d tnormal = trafo.get_matrix().matrix().block(0, 0, 3, 3).inverse().transpose() * normal;
|
||||
const Transform3d rotation = Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ()));
|
||||
instance.set_transformation(Geometry::Transformation(trafo.get_offset_matrix() * rotation * trafo.get_matrix_no_offset()));
|
||||
// Drop this instance only: ensure_on_bed() skips instances without auto_drop and measures the first instance.
|
||||
object.translate_instance(instance_idx, -object.instance_bounding_box(instance_idx).min.z() * Vec3d::UnitZ());
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "Point.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class ModelObject;
|
||||
|
||||
// A face of an object's convex hull that the object can rest on. These are the faces the
|
||||
// "Lay on Face" gizmo offers and the ones the CLI --ground-* options choose from.
|
||||
//
|
||||
// Frames: "object" coordinates have the volume transformations applied but not the instance
|
||||
// transformation. "Instance" coordinates additionally have the instance rotation, scale and
|
||||
// mirror applied, but not its offset.
|
||||
struct LayOnFacePlane
|
||||
{
|
||||
Vec3d normal; // outward unit normal, object coordinates
|
||||
Vec3d center; // centroid of the outline, object coordinates; on the face's mean plane
|
||||
float area; // mm², instance coordinates
|
||||
Pointf3s outline; // convex outline in the plane frame, where the face is horizontal
|
||||
Transform3d to_plane_frame; // rotation from instance coordinates to the plane frame
|
||||
};
|
||||
|
||||
// Candidate faces of the object's model parts, largest first. The instance transformation
|
||||
// (without offset) is applied before measuring, so faces too small to rest on are dropped
|
||||
// by their printed size: under 5 mm², a side under 1 mm, or an inner angle under 1°.
|
||||
std::vector<LayOnFacePlane> lay_on_face_planes(const ModelObject &object, const Transform3d &instance_matrix_no_offset);
|
||||
|
||||
// Index of the largest plane, or -1 if `planes` is empty. Of planes with the same area, such as
|
||||
// the top and bottom of a box, the one already facing down the most wins, so flat parts stay put.
|
||||
int find_largest_plane(const std::vector<LayOnFacePlane> &planes);
|
||||
|
||||
// Index of the plane whose normal is closest to `direction` (object coordinates),
|
||||
// or -1 if `planes` is empty.
|
||||
int find_plane_by_normal(const std::vector<LayOnFacePlane> &planes, const Vec3d &direction);
|
||||
|
||||
// Index of the plane whose face contains `point` (object coordinates) within `tolerance` mm, or -1
|
||||
// if there is none. `instance_matrix_no_offset` is the one the planes were computed with.
|
||||
int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transform3d &instance_matrix_no_offset,
|
||||
const Vec3d &point, double tolerance);
|
||||
|
||||
// Rotates the instance so that `normal` (object coordinates) points down, the same rotation as
|
||||
// the gizmo applies, then drops the instance so its lowest point is at z = 0.
|
||||
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal);
|
||||
|
||||
} // namespace Slic3r
|
||||
+16
-7
@@ -153,6 +153,7 @@ bool Layer::is_perimeter_compatible(const Print& print, const PrintRegion& a, co
|
||||
&& config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) == other_config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id))
|
||||
&& config.filter_out_gap_fill.value == other_config.filter_out_gap_fill.value
|
||||
&& config.detect_overhang_wall == other_config.detect_overhang_wall
|
||||
&& config.unsupported_wall_last == other_config.unsupported_wall_last
|
||||
&& config.overhang_reverse == other_config.overhang_reverse
|
||||
&& config.overhang_reverse_threshold == other_config.overhang_reverse_threshold
|
||||
&& config.wall_direction == other_config.wall_direction
|
||||
@@ -187,6 +188,12 @@ void Layer::make_perimeters()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << "Generating perimeters for layer " << this->id();
|
||||
|
||||
const auto clear_generated_extrusions = [](LayerRegion *layer_region) {
|
||||
layer_region->perimeters.clear();
|
||||
layer_region->fills.clear();
|
||||
layer_region->thin_fills.clear();
|
||||
};
|
||||
|
||||
// keep track of regions whose perimeters we have already generated
|
||||
std::vector<unsigned char> done(m_regions.size(), false);
|
||||
|
||||
@@ -217,13 +224,11 @@ void Layer::make_perimeters()
|
||||
if (this_region.gradient_volume_id() != other_region.gradient_volume_id())
|
||||
continue;
|
||||
if (is_perimeter_compatible(*m_object->print(), this_region, other_region))
|
||||
{
|
||||
other_layerm->perimeters.clear();
|
||||
other_layerm->fills.clear();
|
||||
other_layerm->thin_fills.clear();
|
||||
layerms.push_back(other_layerm);
|
||||
done[it - m_regions.begin()] = true;
|
||||
}
|
||||
{
|
||||
clear_generated_extrusions(other_layerm);
|
||||
layerms.push_back(other_layerm);
|
||||
done[it - m_regions.begin()] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (layerms.size() == 1) { // optimization
|
||||
@@ -231,6 +236,10 @@ void Layer::make_perimeters()
|
||||
(*layerm)->make_perimeters((*layerm)->slices, {*layerm}, &(*layerm)->fill_surfaces, &(*layerm)->fill_no_overlap_expolygons);
|
||||
(*layerm)->fill_expolygons = to_expolygons((*layerm)->fill_surfaces.surfaces);
|
||||
} else {
|
||||
// Orca: Unlike the compatible regions above, the initiating region has not
|
||||
// been cleared yet and may contain paths from a previous incompatible run.
|
||||
clear_generated_extrusions(*layerm);
|
||||
|
||||
SurfaceCollection new_slices;
|
||||
// Use the region with highest infill rate, as the make_perimeters() function below decides on the gap fill based on the infill existence.
|
||||
LayerRegion *layerm_config = layerms.front();
|
||||
|
||||
@@ -16,6 +16,7 @@ using LayerPtrs = std::vector<Layer*>;
|
||||
class LayerRegion;
|
||||
using LayerRegionPtrs = std::vector<LayerRegion*>;
|
||||
class PrintRegion;
|
||||
class PrintRegionConfig;
|
||||
class PrintObject;
|
||||
class Print;
|
||||
|
||||
@@ -200,6 +201,11 @@ public:
|
||||
FillAdaptive::Octree *support_fill_octree,
|
||||
FillLightning::Generator* lightning_generator) const;
|
||||
void make_ironing();
|
||||
// Returns the filament id (1-based) the region is ironed with, or -1 when the
|
||||
// region is not ironed.
|
||||
static int choose_ironing_extruder(const PrintRegionConfig &cfg,
|
||||
bool spiral_mode,
|
||||
bool is_topmost_layer);
|
||||
void make_contour_z(const sla::IndexedMesh &mesh);
|
||||
|
||||
void export_region_slices_to_svg(const char *path) const;
|
||||
|
||||
@@ -30,8 +30,8 @@ bool Line::intersection_infinite(const Line &other, Point* point) const
|
||||
return false;
|
||||
double t1 = cross2(v12, v2) / denom;
|
||||
Vec2d result = (a1 + t1 * v1);
|
||||
if (result.x() > std::numeric_limits<coord_t>::max() || result.x() < std::numeric_limits<coord_t>::lowest() ||
|
||||
result.y() > std::numeric_limits<coord_t>::max() || result.y() < std::numeric_limits<coord_t>::lowest()) {
|
||||
if (result.x() > double(std::numeric_limits<coord_t>::max()) || result.x() < double(std::numeric_limits<coord_t>::lowest()) ||
|
||||
result.y() > double(std::numeric_limits<coord_t>::max()) || result.y() < double(std::numeric_limits<coord_t>::lowest())) {
|
||||
// Intersection has at least one of the coordinates much bigger (or smaller) than coord_t maximum value (or minimum).
|
||||
// So it can not be stored into the Point without integer overflows. That could mean that input lines are parallel or near parallel.
|
||||
return false;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#ifdef _WIN32
|
||||
#include <charconv>
|
||||
#endif
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <fast_float/fast_float.h>
|
||||
|
||||
@@ -60,10 +60,15 @@ auto MinimumSpanningTree::prim(std::vector<Point> vertices) const -> AdjacencyGr
|
||||
//This search is O(V) right now, which can be made down to O(log(V)). This reduces the overall time complexity from O(V*V) to O(V*log(E)).
|
||||
//However that requires an implementation of a heap that supports the decreaseKey operation, which is not in the std library.
|
||||
//TODO: Implement this?
|
||||
// Break equal-distance ties on coordinates: the map is keyed by address, so its
|
||||
// iteration order (and therefore the first minimum) would otherwise depend on where
|
||||
// the vertices were allocated.
|
||||
using MapValue = std::pair<const Point*, coordf_t>;
|
||||
const auto closest = std::min_element(smallest_distance.begin(), smallest_distance.end(),
|
||||
[](const MapValue& a, const MapValue& b) {
|
||||
return a.second < b.second;
|
||||
if (a.second != b.second)
|
||||
return a.second < b.second;
|
||||
return *a.first < *b.first;
|
||||
});
|
||||
|
||||
//Add this point to the graph and remove it from the candidates.
|
||||
|
||||
@@ -3601,6 +3601,15 @@ void FacetsAnnotation::shift_states_above(const ModelVolume &mv, EnforcerBlocker
|
||||
this->set(selector);
|
||||
}
|
||||
|
||||
void FacetsAnnotation::remap_states(const ModelVolume &mv, const EnforcerBlockerStateMap &state_map)
|
||||
{
|
||||
if (empty()) return;
|
||||
TriangleSelector selector(mv.mesh());
|
||||
selector.deserialize(m_data, false);
|
||||
selector.remap_triangle_state(state_map);
|
||||
this->set(selector);
|
||||
}
|
||||
|
||||
void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume &mv,
|
||||
EnforcerBlockerType max_type,
|
||||
EnforcerBlockerType to_delete_filament,
|
||||
@@ -3865,6 +3874,43 @@ bool model_has_advanced_features(const Model &model)
|
||||
return false;
|
||||
}
|
||||
|
||||
void remap_model_filament_slots(Model &model, const std::map<int, int> &slot_relocations)
|
||||
{
|
||||
if (slot_relocations.empty())
|
||||
return;
|
||||
|
||||
// Paint states and the object/volume "extruder" configs store one-based slot numbers
|
||||
// (see Sidebar::on_action_add_filament's insertion remap for the same encoding).
|
||||
std::map<int, int> one_based_slots;
|
||||
for (const auto &[from, to] : slot_relocations)
|
||||
one_based_slots.emplace(from + 1, to + 1);
|
||||
|
||||
EnforcerBlockerStateMap paint_state_map;
|
||||
for (size_t state = 0; state < paint_state_map.size(); ++state)
|
||||
paint_state_map[state] = EnforcerBlockerType(state);
|
||||
for (const auto &[one_based_from, one_based_to] : one_based_slots) {
|
||||
assert(one_based_from >= 0 && size_t(one_based_from) < paint_state_map.size());
|
||||
assert(one_based_to > 0 && size_t(one_based_to) < paint_state_map.size());
|
||||
paint_state_map[size_t(one_based_from)] = EnforcerBlockerType(one_based_to);
|
||||
}
|
||||
|
||||
auto remap_extruder_config = [&one_based_slots](ModelConfig &config) -> bool {
|
||||
const auto it = config.has("extruder") ? one_based_slots.find(config.extruder()) : one_based_slots.end();
|
||||
if (it == one_based_slots.end())
|
||||
return false;
|
||||
config.set("extruder", it->second);
|
||||
return true;
|
||||
};
|
||||
|
||||
for (ModelObject *object : model.objects) {
|
||||
remap_extruder_config(object->config);
|
||||
for (ModelVolume *volume : object->volumes) {
|
||||
remap_extruder_config(volume->config);
|
||||
volume->mmu_segmentation_facets.remap_states(*volume, paint_state_map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique.
|
||||
void check_model_ids_validity(const Model &model)
|
||||
|
||||
@@ -745,6 +745,10 @@ public:
|
||||
// Shift painted filament indices >= threshold by delta. Used when a physical filament is
|
||||
// inserted ahead of existing slots (mixed-color slots are kept at the end of the list).
|
||||
void shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta);
|
||||
// Relabel painted filament indices according to state_map (old state value -> new state
|
||||
// value; untouched states keep their identity). Used when published-3MF import relocates
|
||||
// mixed-filament definitions onto new slot numbers.
|
||||
void remap_states(const ModelVolume &mv, const EnforcerBlockerStateMap &state_map);
|
||||
indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const;
|
||||
bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const;
|
||||
bool empty() const { return m_data.triangles_to_split.empty(); }
|
||||
@@ -1794,6 +1798,13 @@ bool model_has_multi_part_objects(const Model &model);
|
||||
// If the model has advanced features, then it cannot be processed in simple mode.
|
||||
bool model_has_advanced_features(const Model &model);
|
||||
|
||||
// Remap the model's filament-slot references after a published-3MF import relocated
|
||||
// mixed-filament definitions onto new slot numbers: object/volume "extruder" configs and
|
||||
// multi-material color-painting states (paint state stores the one-based slot number).
|
||||
// slot_relocations maps the author's zero-based slot number to its final zero-based slot;
|
||||
// entries are applied simultaneously (no chained lookups), untouched slots keep everything.
|
||||
void remap_model_filament_slots(Model &model, const std::map<int, int> &slot_relocations);
|
||||
|
||||
#ifndef NDEBUG
|
||||
// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique.
|
||||
void check_model_ids_validity(const Model &model);
|
||||
|
||||
@@ -170,6 +170,10 @@ public:
|
||||
this->m_check_sum = rhs.check_sum();
|
||||
this->m_connectors_cnt = rhs.connectors_cnt();
|
||||
}
|
||||
// A user-declared copy assignment or destructor deprecates the implicitly generated
|
||||
// copy constructor, and this class has both, so declare it rather than rely on it.
|
||||
CutObjectBase(const CutObjectBase &) = default;
|
||||
|
||||
CutObjectBase &operator=(const CutObjectBase &other)
|
||||
{
|
||||
this->copy(other);
|
||||
|
||||
@@ -550,6 +550,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
|
||||
if (!paths.empty()) {
|
||||
if (extrusion->is_closed) {
|
||||
ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour ? elrDefault : elrHole);
|
||||
extrusion_loop.inset_idx = extrusion->inset_idx;
|
||||
if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) ==
|
||||
(pg_extrusion.is_contour || pg_extrusions.size() == 2))
|
||||
extrusion_loop.make_counter_clockwise();
|
||||
@@ -1318,6 +1319,73 @@ static void reorient_perimeters(ExtrusionEntityCollection &entities, bool steep_
|
||||
}
|
||||
}
|
||||
|
||||
// A loop made of nothing but overhang paths lies entirely off the lower layer.
|
||||
static bool is_unsupported_loop(const ExtrusionEntity *entity)
|
||||
{
|
||||
if (!entity->is_loop())
|
||||
return false;
|
||||
const ExtrusionPaths &paths = static_cast<const ExtrusionLoop *>(entity)->paths;
|
||||
return !paths.empty() && std::all_of(paths.begin(), paths.end(),
|
||||
[](const ExtrusionPath &path) { return path.role() == erOverhangPerimeter; });
|
||||
}
|
||||
|
||||
// ORCA: A wall loop with nothing under it has nothing to lean on, so whatever the configured wall
|
||||
// sequence it is extruded after the loops that anchor it, innermost first. A loop that runs alongside
|
||||
// an anchored one belongs to the same wall stack and keeps its place ahead of the infill, which needs
|
||||
// it as an anchor; one that touches nothing has only that infill to rest on, so it is flagged for the
|
||||
// G-code writer to hold it back until the infill is down.
|
||||
static void defer_unsupported_loops(const PerimeterGenerator &perimeter_generator, ExtrusionEntityCollection &entities)
|
||||
{
|
||||
if (!perimeter_generator.config->unsupported_wall_last)
|
||||
return;
|
||||
|
||||
ExtrusionEntitiesPtr &src = entities.entities;
|
||||
auto first_deferred = std::stable_partition(src.begin(), src.end(),
|
||||
[](const ExtrusionEntity *entity) { return !is_unsupported_loop(entity); });
|
||||
if (first_deferred == src.end())
|
||||
return;
|
||||
|
||||
std::stable_sort(first_deferred, src.end(),
|
||||
[](const ExtrusionEntity *lhs, const ExtrusionEntity *rhs) { return lhs->inset_idx > rhs->inset_idx; });
|
||||
|
||||
auto collect_lines = [](const ExtrusionEntity *entity, Lines &out) {
|
||||
Polylines polylines;
|
||||
entity->collect_polylines(polylines);
|
||||
append(out, to_lines(polylines));
|
||||
};
|
||||
|
||||
Lines anchored;
|
||||
for (auto it = src.begin(); it != first_deferred; ++it)
|
||||
collect_lines(*it, anchored);
|
||||
|
||||
std::vector<ExtrusionLoop *> unattached;
|
||||
for (auto it = first_deferred; it != src.end(); ++it)
|
||||
unattached.emplace_back(static_cast<ExtrusionLoop *>(*it));
|
||||
|
||||
// A loop leaning on a loop that is itself anchored is anchored as well, so spread outwards from
|
||||
// the anchored loops until no unsupported loop is left touching what was reached.
|
||||
const double touch_distance = 1.5 * std::max(perimeter_generator.ext_perimeter_flow.scaled_spacing(),
|
||||
perimeter_generator.perimeter_flow.scaled_spacing());
|
||||
while (!anchored.empty()) {
|
||||
AABBTreeLines::LinesDistancer<Line> distancer{std::move(anchored)};
|
||||
anchored.clear();
|
||||
for (ExtrusionLoop *&loop : unattached) {
|
||||
if (loop == nullptr)
|
||||
continue;
|
||||
const Points points = loop->as_polyline().points;
|
||||
if (std::any_of(points.begin(), points.end(),
|
||||
[&distancer, touch_distance](const Point &point) { return distancer.distance_from_lines<false>(point) < touch_distance; })) {
|
||||
collect_lines(loop, anchored);
|
||||
loop = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ExtrusionLoop *loop : unattached)
|
||||
if (loop != nullptr)
|
||||
loop->print_after_infill = true;
|
||||
}
|
||||
|
||||
void PerimeterGenerator::process_classic()
|
||||
{
|
||||
group_region_by_fuzzify(*this);
|
||||
@@ -1804,6 +1872,8 @@ void PerimeterGenerator::process_classic()
|
||||
}
|
||||
}
|
||||
|
||||
defer_unsupported_loops(*this, entities);
|
||||
|
||||
// append perimeters for this slice as a collection
|
||||
if (! entities.empty())
|
||||
this->loops->append(entities);
|
||||
@@ -2742,6 +2812,7 @@ void PerimeterGenerator::process_arachne()
|
||||
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
|
||||
this->config->overhang_reverse_internal_only);
|
||||
}
|
||||
defer_unsupported_loops(*this, extrusion_coll);
|
||||
this->loops->append(extrusion_coll);
|
||||
}
|
||||
|
||||
|
||||
@@ -545,7 +545,7 @@ std::string generate_preset_setting_id(const std::string& vendor, const std::str
|
||||
return "";
|
||||
|
||||
// Dedicated namespace for preset setting_ids, distinct from the cloud per-user
|
||||
// namespace (OrcaCloudServiceAgent). Keep in sync with scripts/orca_id_tool.py;
|
||||
// namespace (OrcaCloudServiceAgent). Keep in sync with scripts/orca_profile_tool.py;
|
||||
// never change this constant.
|
||||
static const boost::uuids::uuid vendor_namespace =
|
||||
boost::uuids::string_generator()("c1f4d9e2-7a3b-5c8d-9e0f-1a2b3c4d5e6f");
|
||||
@@ -867,6 +867,20 @@ bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const Pre
|
||||
return is_compatible_with_printer(preset, active_printer, &config);
|
||||
}
|
||||
|
||||
// ORCA: see the header. The CLI resolves --load-settings into bare DynamicPrintConfigs and has no
|
||||
// Preset objects to hand; without this it would have to reimplement the policy or build the shells
|
||||
// at every call site.
|
||||
bool is_compatible_with_printer(const DynamicPrintConfig &preset_config, Preset::Type preset_type,
|
||||
const DynamicPrintConfig &printer_config, const std::string &printer_name)
|
||||
{
|
||||
Preset preset(preset_type, std::string("__compat_check"));
|
||||
preset.config = preset_config;
|
||||
Preset printer(Preset::TYPE_PRINTER, printer_name);
|
||||
printer.config = printer_config;
|
||||
return is_compatible_with_printer(PresetWithVendorProfile(preset, nullptr),
|
||||
PresetWithVendorProfile(printer, nullptr));
|
||||
}
|
||||
|
||||
void Preset::set_visible_from_appconfig(const AppConfig &app_config)
|
||||
{
|
||||
//BBS: add config related log
|
||||
@@ -1044,6 +1058,7 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"reduce_crossing_wall",
|
||||
"detect_thin_wall",
|
||||
"detect_overhang_wall",
|
||||
"unsupported_wall_last",
|
||||
"overhang_reverse",
|
||||
"overhang_reverse_threshold",
|
||||
"overhang_reverse_internal_only",
|
||||
@@ -1268,6 +1283,8 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"accel_to_decel_enable",
|
||||
"accel_to_decel_factor",
|
||||
"wipe_on_loops",
|
||||
"wipe_inward",
|
||||
"wipe_inward_distance",
|
||||
"wipe_before_external_loop",
|
||||
"bridge_density",
|
||||
"internal_bridge_density",
|
||||
@@ -1304,6 +1321,8 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"wipe_tower_extra_flow",
|
||||
"single_extruder_multi_material_priming",
|
||||
"toolchange_ordering",
|
||||
"toolchange_cyclic_order",
|
||||
"toolchange_cyclic_first_layer",
|
||||
"wipe_tower_rotation_angle",
|
||||
"tree_support_branch_distance_organic",
|
||||
"tree_support_branch_diameter_organic",
|
||||
@@ -1429,7 +1448,7 @@ static std::vector<std::string> s_Preset_printer_options {
|
||||
"gcode_skip_config_block", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
|
||||
"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",
|
||||
"printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", "extruder_clearance_dist_to_rod",
|
||||
"nozzle_height", "master_extruder_id",
|
||||
"default_print_profile", "inherits",
|
||||
"silent_mode",
|
||||
@@ -3062,6 +3081,75 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
|
||||
this->get_selected_preset().save(nullptr);
|
||||
}
|
||||
|
||||
// A detached standalone preset for the Full Publish receiver: create a user preset holding
|
||||
// the full resolved filament config (no inheritance, no vendor/alias links), parentless.
|
||||
// Note: universal printer compatibility is not enforced here - callers apply
|
||||
// make_publish_universal() to the config before handing it over when they need it.
|
||||
// Mirrors save_current_preset(detach=true)'s creation branch but does not force-select or
|
||||
// diff against a parent; the caller decides whether to select it.
|
||||
// The published entry's filament_id is forwarded so user bases keep their stable
|
||||
// material grouping (get_filament_presets() groups user bases by filament_id).
|
||||
// The copy is a project-embedded preset: it lives inside the loaded project only
|
||||
// (serialized into the saved .3mf, restored by load_project_embedded_presets) and
|
||||
// never touches the user's library directory; Preset::save() early-returns for
|
||||
// embedded presets, so persistence is skipped here too.
|
||||
// Returns the final (uniquified) name; on collision "<base>" -> "<base> (Published)" ->
|
||||
// "<base> (Published 2)" ...
|
||||
std::string PresetCollection::add_detached_preset(const std::string &name_base, DynamicPrintConfig config,
|
||||
const std::string &filament_id)
|
||||
{
|
||||
if (name_base.empty())
|
||||
return std::string();
|
||||
Preset stored(m_type, name_base);
|
||||
stored.config = std::move(config);
|
||||
stored.filament_id = filament_id;
|
||||
|
||||
// Uniquify verbatim; only on collision append " (Published)" then " (Published 2)".
|
||||
const std::string base_name = name_base;
|
||||
std::string final_name = base_name;
|
||||
auto exists = [this](const std::string &candidate) -> bool {
|
||||
const auto it = this->find_preset_internal(candidate);
|
||||
return it != m_presets.end() && it->name == candidate;
|
||||
};
|
||||
if (exists(final_name)) {
|
||||
final_name = base_name + " (Published)";
|
||||
for (int i = 2; exists(final_name); ++i)
|
||||
final_name = base_name + " (Published " + std::to_string(i) + ")";
|
||||
}
|
||||
|
||||
// Creation branch of save_current_preset(detach=true), without its selection side
|
||||
// effects or project-embedded path.
|
||||
lock();
|
||||
const auto it = this->find_preset_internal(final_name);
|
||||
if (m_presets.begin() + m_idx_selected >= it)
|
||||
++m_idx_selected;
|
||||
Preset &preset = *m_presets.insert(it, stored);
|
||||
preset.name = final_name;
|
||||
preset.vendor = nullptr;
|
||||
preset.alias.clear();
|
||||
preset.renamed_from.clear();
|
||||
preset.m_excluded_from.clear();
|
||||
preset.setting_id.clear();
|
||||
preset.inherits().clear();
|
||||
preset.version = Semver::parse(SoftFever_VERSION).value_or(Semver());
|
||||
preset.is_default = false;
|
||||
preset.is_system = false;
|
||||
preset.is_external = false;
|
||||
preset.bundle_id.clear();
|
||||
preset.file = this->path_for_preset(preset);
|
||||
preset.is_visible = true;
|
||||
preset.is_project_embedded = true;
|
||||
if (m_type == Preset::TYPE_PRINT)
|
||||
preset.config.option<ConfigOptionString>("print_settings_id", true)->value = final_name;
|
||||
else if (m_type == Preset::TYPE_FILAMENT)
|
||||
preset.config.option<ConfigOptionStrings>("filament_settings_id", true)->values[0] = final_name;
|
||||
else if (m_type == Preset::TYPE_PRINTER)
|
||||
preset.config.option<ConfigOptionString>("printer_settings_id", true)->value = final_name;
|
||||
unlock();
|
||||
|
||||
return final_name;
|
||||
}
|
||||
|
||||
bool PresetCollection::delete_current_preset()
|
||||
{
|
||||
Preset &selected = this->get_selected_preset();
|
||||
|
||||
@@ -93,8 +93,8 @@ class PresetBundle;
|
||||
|
||||
// Deterministic preset setting_id: uuid5(vendor/type/name) -> 16 base62 chars.
|
||||
// Pure function of a system preset's identity, so the value can be assigned by
|
||||
// scripts/orca_id_tool.py and recomputed here when a profile ships without it.
|
||||
// MUST stay byte-identical to scripts/orca_id_tool.py.
|
||||
// scripts/orca_profile_tool.py and recomputed here when a profile ships without it.
|
||||
// MUST stay byte-identical to scripts/orca_profile_tool.py.
|
||||
// This is NOT the per-user cloud-sync setting_id
|
||||
// (OrcaCloudServiceAgent::generate_uuid_for_setting_id) - do not conflate them.
|
||||
std::string generate_preset_setting_id(const std::string& vendor,
|
||||
@@ -459,6 +459,11 @@ protected:
|
||||
bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer);
|
||||
bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer, const DynamicPrintConfig *extra_config);
|
||||
bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer);
|
||||
// ORCA: same check for callers that hold raw configs rather than Presets (the CLI). Wraps them in
|
||||
// throwaway Preset shells and delegates, so the compatibility policy -- including the fail-open on a
|
||||
// malformed compatible_printers_condition -- lives in one place for the GUI and the CLI alike.
|
||||
bool is_compatible_with_printer(const DynamicPrintConfig &preset_config, Preset::Type preset_type,
|
||||
const DynamicPrintConfig &printer_config, const std::string &printer_name);
|
||||
|
||||
// Where a preset is being loaded from. `Auto` lets load_presets() infer from the directory path.
|
||||
struct PresetOrigin {
|
||||
@@ -631,6 +636,22 @@ public:
|
||||
// All presets are marked as not modified and the new preset is activated.
|
||||
//BBS: add project embedded preset logic
|
||||
void save_current_preset(const std::string &new_name, bool detach = false, bool save_to_project = false, Preset* _curr_preset = nullptr);
|
||||
// Insert a standalone user preset holding the full resolved config (no inheritance,
|
||||
// no vendor links): the libslic3r equivalent of "Detach from parent". Takes a
|
||||
// resolved config, clears parent/vendor/alias metadata, stamps filament_settings_id.
|
||||
// Unlike save_current_preset it does not force-select or diff against a parent.
|
||||
// Used by the published-3MF Full Publish path. The optional filament_id seeds the
|
||||
// preset's stable material grouping (get_filament_presets groups user bases by
|
||||
// filament_id); the published entry's filament_id is forwarded so the copy keeps
|
||||
// the author's grouping.
|
||||
// The copy is a project-embedded preset ("Preset Inside Project"): it lives inside
|
||||
// the loaded project only, is serialized into the saved .3mf via
|
||||
// get_current_project_embedded_presets(), and is never written to the user's
|
||||
// library directory.
|
||||
// Returns the final (uniquified) name; on collision the suffix rule is:
|
||||
// "<base>" -> "<base> (Published)" -> "<base> (Published 2)" ...
|
||||
std::string add_detached_preset(const std::string &name_base, DynamicPrintConfig config,
|
||||
const std::string &filament_id = std::string());
|
||||
|
||||
// Delete the current preset, activate the first visible preset.
|
||||
// returns true if the preset was deleted successfully.
|
||||
|
||||
+1558
-173
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,14 @@
|
||||
#include "Preset.hpp"
|
||||
#include "PresetCacheFormat.hpp"
|
||||
#include "AppConfig.hpp"
|
||||
#include "PublishSettings.hpp"
|
||||
#include "enum_bitmask.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <shared_mutex>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <array>
|
||||
@@ -168,6 +171,30 @@ struct PresetBundleMetadata
|
||||
}
|
||||
};
|
||||
|
||||
// A "published" 3MF project: keeps the user's currently-selected presets and overlays only the
|
||||
// author-selected published keys onto the edited presets.
|
||||
struct PublishedConfig
|
||||
{
|
||||
bool published = false;
|
||||
std::vector<std::string> published_keys;
|
||||
// Per-slot published material keys, applied positionally (author slot N -> receiver slot N).
|
||||
// Partial entries are gated by the author's optional type requirement and written onto the
|
||||
// slot's stored preset in place; full entries instead detach (see PublishedMaterialEntry in
|
||||
// PublishSettings.hpp).
|
||||
std::vector<PublishedMaterialEntry> material_keys;
|
||||
// Keys that could not be applied (missing on the user's machine or vector size mismatch),
|
||||
// filled in by load_config_file_config for notification purposes.
|
||||
std::vector<std::string> skipped_keys;
|
||||
// Human-readable notices of the slot material replacements performed while loading a
|
||||
// published project, for the load notification.
|
||||
std::vector<std::string> material_replacements;
|
||||
// Mixed-filament entries that had to be moved off their authored slot on load (a real,
|
||||
// physical filament occupied it): maps the author's zero-based slot number to its final
|
||||
// zero-based slot. Consumers (e.g. model extruder/color-painting remapping) use this to
|
||||
// keep geometry references pointing at the relocated definitions.
|
||||
std::map<int, int> mixed_slot_relocations;
|
||||
};
|
||||
|
||||
// Bundle of Print + Filament + Printer presets.
|
||||
class PresetBundle
|
||||
{
|
||||
@@ -464,8 +491,8 @@ public:
|
||||
|
||||
// Load configuration that comes from a model file containing configuration, such as 3MF et al.
|
||||
// This method is called by the Plater.
|
||||
void load_config_model(const std::string &name, DynamicPrintConfig config, Semver file_version = Semver())
|
||||
{ this->load_config_file_config(name, true, std::move(config), file_version); }
|
||||
void load_config_model(const std::string &name, DynamicPrintConfig config, Semver file_version = Semver(), PublishedConfig *published_config = nullptr)
|
||||
{ this->load_config_file_config(name, true, std::move(config), file_version, false, published_config); }
|
||||
|
||||
// Load an external config file containing the print, filament and printer presets.
|
||||
// Instead of a config file, a G-code may be loaded containing the full set of parameters.
|
||||
@@ -590,6 +617,11 @@ public:
|
||||
// compatible_prints references a deleted (unknown) or renamed (old) preset name.
|
||||
bool check_preset_references() const;
|
||||
|
||||
// Validator-only: every system FFF printer variant needs a compatible system filament
|
||||
// named in its model's default_materials, every name there and in the printer's
|
||||
// default_filament_profile must resolve to a system filament.
|
||||
bool check_printer_default_materials() const;
|
||||
|
||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
||||
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
|
||||
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
|
||||
@@ -626,6 +658,17 @@ private:
|
||||
bool m_generate_vendor_caches { false };
|
||||
bool m_preserve_vendor_source_paths { false };
|
||||
|
||||
// Vendor trees loaded by resolve_preset_config's manifest path, so every preset
|
||||
// resolved through this bundle shares one load per source root and vendor. The
|
||||
// filament library is one such tree, shared by every vendor under its root.
|
||||
std::map<std::tuple<std::string, std::string, ForwardCompatibilitySubstitutionRule>, std::unique_ptr<PresetBundle>>
|
||||
m_source_vendor_bundles;
|
||||
|
||||
const PresetBundle *load_source_vendor(const boost::filesystem::path &root_dir,
|
||||
const std::string &vendor_id,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
||||
std::string &error);
|
||||
|
||||
// Orca: validation only - flag any printer with two or more compatible
|
||||
// filament presets sharing one filament_id (ambiguous AMS subtype match).
|
||||
bool check_duplicate_filament_subtypes() const;
|
||||
@@ -646,7 +689,7 @@ private:
|
||||
// Load print, filament & printer presets from a config. If it is an external config, then the name is extracted from the external path.
|
||||
// and the external config is just referenced, not stored into user profile directory.
|
||||
// If it is not an external config, then the config will be stored into the user profile directory.
|
||||
void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version = Semver(), bool selected = false);
|
||||
void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version = Semver(), bool selected = false, PublishedConfig *published_config = nullptr);
|
||||
/*ConfigSubstitutions load_config_file_config_bundle(
|
||||
const std::string &path, const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule);*/
|
||||
|
||||
|
||||
+495
-98
@@ -20,6 +20,7 @@
|
||||
#include "GCode.hpp"
|
||||
#include "GCode/WipeTower.hpp"
|
||||
#include "GCode/WipeTower2.hpp"
|
||||
#include "GCode/WipeTowerEstimate.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "MaterialType.hpp"
|
||||
@@ -232,6 +233,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
"accel_to_decel_enable",
|
||||
"accel_to_decel_factor",
|
||||
"wipe_on_loops",
|
||||
"wipe_inward",
|
||||
"wipe_inward_distance",
|
||||
"gcode_comments",
|
||||
"gcode_label_objects",
|
||||
"exclude_object",
|
||||
@@ -357,6 +360,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
|| opt_key == "other_layers_print_sequence"
|
||||
|| opt_key == "other_layers_print_sequence_nums"
|
||||
|| opt_key == "toolchange_ordering"
|
||||
|| opt_key == "toolchange_cyclic_order"
|
||||
|| opt_key == "toolchange_cyclic_first_layer"
|
||||
|| opt_key == "extruder_ams_count"
|
||||
|| opt_key == "extruder_nozzle_stats"
|
||||
|| opt_key == "filament_map_mode"
|
||||
@@ -961,6 +966,377 @@ StringObjectException Print::sequential_print_clearance_valid(const Print &print
|
||||
return single_object_exception;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Clearance rule for a prime tower compacted by wipe_tower_no_sparse_layers.
|
||||
// Ported from BambuStudio and adapted to Orca's printer config: Orca has no
|
||||
// prime_tower_lift_height (z_hop alone bounds the spiral), spells the toolhead radius
|
||||
// extruder_clearance_radius, and derives the spiral slope from the per-filament travel_slope instead
|
||||
// of one global constant.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
double compacted_tower_footprint_padding(const PrintConfig &config, double brim_width)
|
||||
{
|
||||
// The brim is deposited material like any other and reaches past the wall on the first layer, so
|
||||
// the sweeping rod has to clear it too.
|
||||
//
|
||||
// On top of it, two effects make a nominal outline fall short of the printed tower on its low
|
||||
// corner even though it overshoots by millimetres on the high one: WipeTower re-centres the tower
|
||||
// by rib_offset once its first-layer wall is known, and the precise check hulls extrusion centre
|
||||
// lines, so the deposited material reaches half a line width further still. Allowing a line width
|
||||
// per side covers both, which is what keeps an estimated footprint enclosing the real one and the
|
||||
// pre-slice check stricter than the precise one.
|
||||
return std::max(0., brim_width) + 2. * config.nozzle_diameter.get_at(0);
|
||||
}
|
||||
|
||||
Polygons compacted_wipe_tower_rings(const CompactedTowerZone &zone, bool any_body_tier)
|
||||
{
|
||||
Polygons rings = zone.grown_nozzle;
|
||||
if (any_body_tier)
|
||||
append(rings, zone.grown_body);
|
||||
return rings;
|
||||
}
|
||||
|
||||
CompactedTowerZone compacted_wipe_tower_zone(const PrintConfig &config, const Polygon &tower_footprint)
|
||||
{
|
||||
CompactedTowerZone zone;
|
||||
if (tower_footprint.points.empty())
|
||||
return zone;
|
||||
|
||||
// Spiral Z-hop at wipe-tower entry (the G3 Z I J that GCodeWriter emits for a SpiralLift) starts on
|
||||
// the tower outline at a low Z. The spiral centre sits one radius away from the start point, so the
|
||||
// circle reaches 2 * radius beyond the outline. radius = lift / (2*pi*atan(travel_slope)) is the
|
||||
// same formula GCodeWriter uses; both are per filament, so take the widest any filament can make.
|
||||
double spiral_reach = 0.;
|
||||
for (size_t i = 0; i < config.z_hop.size(); ++i) {
|
||||
const double lift = std::min(double(config.z_hop.get_at(i)), 5.);
|
||||
if (lift < EPSILON)
|
||||
continue;
|
||||
const double slope = i < config.travel_slope.size() ? double(config.travel_slope.get_at(i)) : 0.;
|
||||
if (slope < EPSILON)
|
||||
continue;
|
||||
spiral_reach = std::max(spiral_reach, 2. * lift / (2. * PI * std::atan(slope)));
|
||||
}
|
||||
|
||||
// Working footprint = outline grown by the spiral envelope. All later clearance tests use this, so
|
||||
// a travel that leaves the deposited wall at low Z is still treated as part of the tower.
|
||||
zone.hull = tower_footprint;
|
||||
if (spiral_reach > EPSILON) {
|
||||
const Polygons grown = offset(tower_footprint, float(scale_(spiral_reach)), jtRound, scale_(0.1));
|
||||
if (! grown.empty())
|
||||
zone.hull = Geometry::convex_hull(grown);
|
||||
}
|
||||
|
||||
// The rod sweeps the whole X axis, so its keep-out band is the tower's Y span widened by half
|
||||
// the nozzle-to-rod offset per side (the instance carries the other half). Orca's sequential
|
||||
// check has no such margin, having had no option to read it from until now.
|
||||
zone.bbox_rod = zone.hull.bounding_box();
|
||||
zone.bbox_rod.offset(scale_(config.extruder_clearance_dist_to_rod.value * 0.5));
|
||||
|
||||
// Horizontal clearance, mirroring the sequential print check down to how the distance is split:
|
||||
// there each of the two object hulls grows by half of extruder_clearance_radius, so the two
|
||||
// outlines touch exactly when the objects are the full radius apart. Splitting it the same way
|
||||
// here (half on the tower, half on the instance in compacted_wipe_tower_clearance) states the
|
||||
// same criterion, and it is what lets the plater draw both outlines: they meet at the instant the
|
||||
// check trips, instead of one of them being already buried inside the other. The smaller
|
||||
// MAX_OUTER_NOZZLE_DIAMETER tier is the bare nozzle cone, the only part narrow enough to sit
|
||||
// beside an object rising less than nozzle_height. The 0.2 mm shaved off is the same rounding
|
||||
// slack the sequential check applies, 0.1 mm per side. Both rings are built here; which one a
|
||||
// given object is measured against depends on its own height and is decided in
|
||||
// compacted_wipe_tower_clearance().
|
||||
zone.body_radius = config.extruder_clearance_radius.value;
|
||||
zone.grown_body = offset(zone.hull, float(scale_(compacted_tower_half_clearance(zone.body_radius))), jtRound, scale_(0.1));
|
||||
zone.grown_nozzle = offset(zone.hull, float(scale_(compacted_tower_half_clearance(MAX_OUTER_NOZZLE_DIAMETER))), jtRound, scale_(0.1));
|
||||
return zone;
|
||||
}
|
||||
|
||||
CompactedTowerClearance compacted_wipe_tower_clearance(const PrintConfig &config, const CompactedTowerZone &zone,
|
||||
const Polygon &inst_hull, double object_rise)
|
||||
{
|
||||
BoundingBox inst_bbox = inst_hull.bounding_box();
|
||||
inst_bbox.offset(scale_(config.extruder_clearance_dist_to_rod.value * 0.5));
|
||||
|
||||
// Only the Y span matters for the rod: it spans the whole X axis, so an object sharing the tower's
|
||||
// Y band passes under it however far apart the two are in X.
|
||||
const bool overlaps_in_y = std::min(inst_bbox.max.y(), zone.bbox_rod.max.y()) - std::max(inst_bbox.min.y(), zone.bbox_rod.min.y()) > 0;
|
||||
|
||||
CompactedTowerClearance result;
|
||||
result.far_clearance = overlaps_in_y ? config.extruder_clearance_height_to_rod.value : config.extruder_clearance_height_to_lid.value;
|
||||
|
||||
// The rod and the lid are the only obstacles once the object stands far enough away. Closer than
|
||||
// the toolhead radius it is the head body itself that hits the object, and it does so as soon as
|
||||
// the object rises past the nozzle cone, which is far below the rod.
|
||||
// The instance carries the other half of each clearance, the tower rings already hold the first
|
||||
// half; see compacted_wipe_tower_zone(). Both halves are needed for the verdict to mean
|
||||
// "a full radius apart", and drawing what is tested is what keeps the plater honest.
|
||||
//
|
||||
// Which tier applies is a property of this object alone: the head body sits above the nozzle cone,
|
||||
// so it cannot reach an object that stays below nozzle_height however close it stands, and however
|
||||
// tall the rest of the plate is.
|
||||
const bool object_is_short = object_rise <= double(config.nozzle_height.value) + EPSILON;
|
||||
result.body_clearance = object_is_short ? double(MAX_OUTER_NOZZLE_DIAMETER) : zone.body_radius;
|
||||
|
||||
const Polygons inst_near_nozzle = offset(inst_hull, float(scale_(compacted_tower_half_clearance(MAX_OUTER_NOZZLE_DIAMETER))), jtRound, scale_(0.1));
|
||||
const bool near_nozzle = ! intersection(zone.grown_nozzle, inst_near_nozzle).empty();
|
||||
result.near_body = false;
|
||||
if (! object_is_short) {
|
||||
const Polygons inst_near_body = offset(inst_hull, float(scale_(compacted_tower_half_clearance(zone.body_radius))), jtRound, scale_(0.1));
|
||||
result.near_body = ! intersection(zone.grown_body, inst_near_body).empty();
|
||||
}
|
||||
|
||||
result.allowed_rise = result.far_clearance;
|
||||
if (near_nozzle)
|
||||
result.allowed_rise = 0.;
|
||||
else if (result.near_body)
|
||||
result.allowed_rise = std::min(result.far_clearance, double(config.nozzle_height.value));
|
||||
return result;
|
||||
}
|
||||
|
||||
Polygon compacted_wipe_tower_offender_outline(const Polygon &inst_hull, double body_clearance)
|
||||
{
|
||||
// Exactly the half-clearance the check grew this instance by, so the halo drawn around an object is
|
||||
// the very outline that was tested against the tower ring of the same tier. Passing the clearance
|
||||
// the object was actually judged on keeps a short object from being drawn with the wide ring it is
|
||||
// not subject to.
|
||||
const Polygons grown = offset(inst_hull, float(scale_(compacted_tower_half_clearance(body_clearance))), jtRound, scale_(0.1));
|
||||
return grown.empty() ? inst_hull : grown.front();
|
||||
}
|
||||
|
||||
// Shared user-facing message for every compacted-tower clearance failure. Height-limit and too-close
|
||||
// are the same class of layout violation under "No sparse layers", so they share one wording.
|
||||
static std::string compacted_wipe_tower_clearance_error()
|
||||
{
|
||||
return L("The relative position of the model and the prime tower does not meet the requirements of the \"No sparse layers\" feature. Please adjust their relative positions, lower the model height, or turn off \"No sparse layers\".");
|
||||
}
|
||||
|
||||
// Convex hull of one print instance in bed coordinates, the same outline both compacted tower checks
|
||||
// compare against the tower.
|
||||
static Polygon compacted_tower_print_instance_hull(const PrintObject &object, const PrintInstance &instance)
|
||||
{
|
||||
Points pts;
|
||||
for (const ModelVolume *v : object.model_object()->volumes) {
|
||||
if (! v->is_model_part())
|
||||
continue;
|
||||
Polygon hull = v->get_convex_hull_2d(Geometry::assemble_transform(Vec3d::Zero(), instance.model_instance->get_rotation(),
|
||||
instance.model_instance->get_scaling_factor(), instance.model_instance->get_mirror()));
|
||||
hull.translate(instance.shift - object.center_offset());
|
||||
append(pts, hull.points);
|
||||
}
|
||||
return pts.empty() ? Polygon() : Geometry::convex_hull(pts);
|
||||
}
|
||||
|
||||
// Footprint the compacted prime tower is expected to occupy on the plate, in bed coordinates.
|
||||
// Before psWipeTower has run there is no tower geometry at all, so this falls back to the same
|
||||
// estimate the plater builds its preview box from. Answering while the user is still arranging the
|
||||
// plate is the whole point of the pre-slice check, and an estimate is all that can be had then.
|
||||
static Polygon estimated_wipe_tower_footprint(const Print &print)
|
||||
{
|
||||
const PrintConfig &config = print.config();
|
||||
const size_t filaments_cnt = print.extruders().size();
|
||||
if (filaments_cnt == 0)
|
||||
return Polygon();
|
||||
|
||||
const WipeTowerData &wtd = print.wipe_tower_data(filaments_cnt);
|
||||
|
||||
double width, depth, brim;
|
||||
Vec2d local_min;
|
||||
if (wtd.bbx.size().x() > EPSILON && wtd.bbx.size().y() > EPSILON) {
|
||||
// The tower has already been generated once, so use its real box (brim included) instead of
|
||||
// re-estimating. Same frame first_layer_wipe_tower_corners() works in.
|
||||
width = wtd.bbx.size().x();
|
||||
depth = wtd.bbx.size().y();
|
||||
local_min = wtd.bbx.min + wtd.rib_offset.cast<double>();
|
||||
brim = 0.;
|
||||
} else {
|
||||
depth = wtd.depth;
|
||||
if (depth < EPSILON)
|
||||
return Polygon();
|
||||
// PartPlate::estimate_wipe_tower_size() squares the rib tower off and the preview box the user
|
||||
// drags around is built from that, so match it here rather than keeping the nominal width.
|
||||
width = config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib ? depth : double(config.prime_tower_width.value);
|
||||
local_min = Vec2d::Zero();
|
||||
brim = double(wtd.brim_width);
|
||||
}
|
||||
|
||||
const double padding = compacted_tower_footprint_padding(config, brim);
|
||||
local_min -= Vec2d(padding, padding);
|
||||
width += 2. * padding;
|
||||
depth += 2. * padding;
|
||||
|
||||
const Eigen::Rotation2Dd rot(Geometry::deg2rad(config.wipe_tower_rotation_angle.value));
|
||||
const Vec2d translate(config.wipe_tower_x.get_at(print.get_plate_index()) + print.get_plate_origin()(0),
|
||||
config.wipe_tower_y.get_at(print.get_plate_index()) + print.get_plate_origin()(1));
|
||||
|
||||
Polygon footprint;
|
||||
for (const Vec2d &corner : { local_min,
|
||||
Vec2d(local_min.x() + width, local_min.y()),
|
||||
Vec2d(local_min.x() + width, local_min.y() + depth),
|
||||
Vec2d(local_min.x(), local_min.y() + depth) }) {
|
||||
const Vec2d p = rot * corner + translate;
|
||||
footprint.points.emplace_back(scale_(p.x()), scale_(p.y()));
|
||||
}
|
||||
return footprint;
|
||||
}
|
||||
|
||||
// Pre-slice counterpart of validate_compacted_wipe_tower_clearance(). It applies the very same
|
||||
// clearance rule, but to an estimated tower footprint instead of the real tool-change extrusions,
|
||||
// which is what lets it run from Print::validate() before anything has been sliced. Reporting through
|
||||
// polygons / height_polygons rather than by throwing is what puts the collision area and the height
|
||||
// limit plane on the plater, exactly the way sequential printing does it.
|
||||
StringObjectException Print::compacted_wipe_tower_clearance_valid(const Print &print, Polygons *polygons, std::vector<std::pair<Polygon, float>> *height_polygons)
|
||||
{
|
||||
const PrintConfig &config = print.config();
|
||||
if (! wipe_tower_sparse_layers_skipped(config) || config.print_sequence != PrintSequence::ByLayer || ! print.has_wipe_tower())
|
||||
return {};
|
||||
|
||||
const CompactedTowerZone zone = compacted_wipe_tower_zone(config, estimated_wipe_tower_footprint(print));
|
||||
if (zone.empty())
|
||||
return {};
|
||||
|
||||
StringObjectException exception;
|
||||
Polygons offenders;
|
||||
bool body_tier_used = false;
|
||||
for (const PrintObject *object : print.objects()) {
|
||||
const double object_top = unscaled<double>(object->max_z());
|
||||
for (const PrintInstance &instance : object->instances()) {
|
||||
const Polygon inst_hull = compacted_tower_print_instance_hull(*object, instance);
|
||||
if (inst_hull.points.empty())
|
||||
continue;
|
||||
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(config, zone, inst_hull, object_top);
|
||||
body_tier_used = body_tier_used || compacted_tower_body_tier(clearance);
|
||||
// Every tier the precise check applies is applied here too, otherwise an object standing
|
||||
// within the toolhead radius would pass here and then be rejected mid-slice, which is the
|
||||
// one outcome this check exists to prevent. The compacted tower base is unknown before
|
||||
// slicing, so the rise is measured from the plate rather than from the tower top; that
|
||||
// overstates it by the tower's own height and makes this check err strict, never lax.
|
||||
if (object_top <= clearance.allowed_rise + EPSILON)
|
||||
continue;
|
||||
|
||||
// Height-limit and too-close cases share one user-facing message: both mean the layout
|
||||
// violates the "No sparse layers" clearance rule, and the remedies are the same.
|
||||
const std::string msg = compacted_wipe_tower_clearance_error();
|
||||
if (exception.string.empty()) {
|
||||
exception.string = msg;
|
||||
exception.object = instance.model_instance;
|
||||
} else {
|
||||
// Same wording for every offender; keep a single copy and drop the object pointer.
|
||||
exception.object = nullptr;
|
||||
}
|
||||
const Polygon outline = compacted_wipe_tower_offender_outline(inst_hull, clearance.body_clearance);
|
||||
offenders.emplace_back(outline);
|
||||
if (height_polygons)
|
||||
height_polygons->emplace_back(outline, float(clearance.allowed_rise));
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the tower's keep-out ring alongside the offending objects, so the collision area reads as
|
||||
// "this object reaches into the space the toolhead needs around the tower" rather than as a lone
|
||||
// highlighted object. Emitted only on a real collision; the plater discards polygons otherwise.
|
||||
// Only the rings some object on this plate is actually measured against are drawn, so that a ring
|
||||
// and an object outline touching always means that object is over its limit.
|
||||
if (polygons && ! offenders.empty()) {
|
||||
append(*polygons, compacted_wipe_tower_rings(zone, body_tier_used));
|
||||
append(*polygons, offenders);
|
||||
}
|
||||
return exception;
|
||||
}
|
||||
|
||||
// With wipe_tower_no_sparse_layers the tower only grows on layers that carry a real toolchange,
|
||||
// so it ends up far below the object and the nozzle has to descend to it. While the nozzle sits
|
||||
// down on the compacted tower the rod is at tower_z + extruder_clearance_height_to_rod, and it
|
||||
// sweeps the tower's Y band across the whole X axis. Anything already printed above that line and
|
||||
// sharing the band gets hit. Nearer than the toolhead radius the head body hits the object well before
|
||||
// the rod does, which is the horizontal half of the same problem. The spiral Z-hop that opens a wipe-
|
||||
// tower travel also leaves the extrusion outline at a low Z, so the footprint used here is the
|
||||
// deposited hull grown by the spiral circle's maximum reach. This mirrors both clearance checks of
|
||||
// sequential printing, except that the tower is revisited over and over, so every object is compared
|
||||
// against it.
|
||||
void Print::validate_compacted_wipe_tower_clearance() const
|
||||
{
|
||||
// Nothing to check when the tower is not compacted: it then follows the object as usual and the
|
||||
// regular by-layer clearance check already covers it. Asking wipe_tower_sparse_layers_skipped()
|
||||
// rather than the raw option keeps this from rejecting plates whose tower is in fact full height.
|
||||
if (! wipe_tower_sparse_layers_skipped(m_config) || m_config.print_sequence != PrintSequence::ByLayer)
|
||||
return;
|
||||
|
||||
const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes = m_wipe_tower_data.tool_changes;
|
||||
if (tool_changes.empty() || m_objects.empty())
|
||||
return;
|
||||
|
||||
// Same accumulation the G-code emitter runs, so validation and output cannot disagree.
|
||||
const std::vector<float> tower_z = compute_compacted_wipe_tower_z(tool_changes, float(m_config.z_offset.value));
|
||||
|
||||
// Wipe tower footprint: build it from the ACTUAL tool-change extrusions rather than the nominal
|
||||
// width x depth rectangle returned by first_layer_wipe_tower_corners(). With a rib wall the printed
|
||||
// wall bulges past the nominal box and the first-layer brim reaches even further; the nominal box
|
||||
// (m_wipe_tower_data.bbx) undercounts that outermost extent by several millimetres, which is
|
||||
// exactly the extent that decides how close the sweeping rod comes to a neighbouring object. The
|
||||
// extrusion end-points are stored in the wipe-tower local frame, so we map them to the bed frame
|
||||
// with the same transform the G-code emitter applies. The two emitters differ in where rib_offset
|
||||
// enters: WipeTowerIntegration::append_tcr() (type 1) rotates the point and then adds the offset,
|
||||
// append_tcr2() (type 2) adds it before rotating. On a rotated rib-wall tower the two land several
|
||||
// millimetres apart, which is exactly the margin this check measures, so follow the emitter in use.
|
||||
const Eigen::Rotation2Dd wt_rot(Geometry::deg2rad(m_config.wipe_tower_rotation_angle.value));
|
||||
const Vec2d wt_translate(m_config.wipe_tower_x.get_at(m_plate_index) + m_origin(0),
|
||||
m_config.wipe_tower_y.get_at(m_plate_index) + m_origin(1));
|
||||
const Vec2d rib_off = m_wipe_tower_data.rib_offset.cast<double>();
|
||||
const bool rib_off_rotates = this->wipe_tower_type() == WipeTowerType::Type2;
|
||||
auto to_bed = [&wt_rot, &wt_translate, &rib_off, rib_off_rotates](const Vec2d &pt) {
|
||||
return rib_off_rotates ? Vec2d(wt_rot * (pt + rib_off) + wt_translate) : Vec2d(wt_rot * pt + wt_translate + rib_off);
|
||||
};
|
||||
|
||||
Points tower_pts;
|
||||
for (const std::vector<WipeTower::ToolChangeResult> &layer : tool_changes) {
|
||||
if (layer.empty() || wipe_tower_layer_is_sparse(layer))
|
||||
continue;
|
||||
for (const WipeTower::ToolChangeResult &tcr : layer)
|
||||
for (size_t i = 0; i < tcr.extrusions.size(); ++i) {
|
||||
// A zero width marks a travel end-point. Keep it only when it opens a real extrusion, so
|
||||
// the hull covers the deposited material and nothing else; travels reach a bit further out
|
||||
// than the walls do.
|
||||
const WipeTower::Extrusion &e = tcr.extrusions[i];
|
||||
if (e.width == 0.f && (i + 1 == tcr.extrusions.size() || tcr.extrusions[i + 1].width == 0.f))
|
||||
continue;
|
||||
const Vec2d p = to_bed(Vec2d(e.pos.x(), e.pos.y()));
|
||||
tower_pts.emplace_back(scale_(p.x()), scale_(p.y()));
|
||||
}
|
||||
}
|
||||
if (tower_pts.empty())
|
||||
return;
|
||||
|
||||
const CompactedTowerZone zone = compacted_wipe_tower_zone(m_config, Geometry::convex_hull(tower_pts));
|
||||
if (zone.empty())
|
||||
return;
|
||||
|
||||
for (const PrintObject *object : m_objects) {
|
||||
const double object_top = unscaled<double>(object->max_z());
|
||||
for (const PrintInstance &instance : object->instances()) {
|
||||
const Polygon inst_hull = compacted_tower_print_instance_hull(*object, instance);
|
||||
if (inst_hull.points.empty())
|
||||
continue;
|
||||
|
||||
// Report the worst layer rather than the first offending one, it is the one that explains the
|
||||
// collision best. The rise has to be known before the clearance: it is what selects the
|
||||
// horizontal tier, the nozzle cone being out of the head body's reach.
|
||||
double max_rise = 0.;
|
||||
for (size_t i = 0; i < tool_changes.size(); ++i) {
|
||||
if (tool_changes[i].empty() || wipe_tower_layer_is_sparse(tool_changes[i]))
|
||||
continue;
|
||||
// Nothing above the current layer exists yet, so a tall object only counts up to it.
|
||||
const double rise = std::min(object_top, double(tool_changes[i].front().print_z)) - tower_z[i];
|
||||
if (rise > max_rise)
|
||||
max_rise = rise;
|
||||
}
|
||||
|
||||
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(m_config, zone, inst_hull, max_rise);
|
||||
if (max_rise <= clearance.allowed_rise + EPSILON)
|
||||
continue;
|
||||
// Same wording as compacted_wipe_tower_clearance_valid(): height-limit and too-close
|
||||
// share one message, since both are layout violations of "No sparse layers".
|
||||
throw Slic3r::SlicingError(compacted_wipe_tower_clearance_error());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//BBS
|
||||
static StringObjectException layered_print_cleareance_valid(const Print &print, StringObjectException *warning)
|
||||
{
|
||||
@@ -1031,20 +1407,21 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
|
||||
|
||||
//BBS: add the wipe tower check logic
|
||||
const PrintConfig & config = print.config();
|
||||
int filaments_count = print.extruders().size();
|
||||
// Custom G-code tool changes (MultiAsSingle) build a real tower on a plate whose objects
|
||||
// all use one filament, so they have to be counted or the hull below collapses to a point.
|
||||
int filaments_count = print.extruders(true).size();
|
||||
int plate_index = print.get_plate_index();
|
||||
const Vec3d plate_origin = print.get_plate_origin();
|
||||
float x = config.wipe_tower_x.get_at(plate_index) + plate_origin(0);
|
||||
float y = config.wipe_tower_y.get_at(plate_index) + plate_origin(1);
|
||||
float width = config.prime_tower_width.value;
|
||||
float a = config.wipe_tower_rotation_angle.value;
|
||||
//float v = config.wiping_volume.value;
|
||||
|
||||
float depth = print.wipe_tower_data(filaments_count).depth;
|
||||
//float brim_width = print.wipe_tower_data(filaments_count).brim_width;
|
||||
|
||||
if (config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib)
|
||||
width = depth;
|
||||
// The estimate resolves the effective width (a rib wall squares the tower).
|
||||
const WipeTowerData &wipe_tower_estimate = print.wipe_tower_data(filaments_count);
|
||||
float width = wipe_tower_estimate.width;
|
||||
float depth = wipe_tower_estimate.depth;
|
||||
float brim_width = wipe_tower_estimate.brim_width;
|
||||
|
||||
Polygons convex_hulls_temp;
|
||||
if (print.has_wipe_tower()) {
|
||||
@@ -1066,36 +1443,54 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
|
||||
convex_hulls_temp.push_back(wipe_tower_polygon);
|
||||
}
|
||||
}
|
||||
// Post-generation the mesh bottom already carries the brim. Pre-generation the body grows
|
||||
// by the brim only when its width is explicit; the auto brim and a Type2 cone base depend on
|
||||
// the tower height, exact only once generated, so they only warn here - the exact footprint
|
||||
// is re-checked in _make_wipe_tower.
|
||||
const bool exact_footprint = print.is_step_done(psWipeTower);
|
||||
Polygons tower_polys_checked = (!exact_footprint && config.prime_tower_brim_width.value >= 0) ?
|
||||
offset(convex_hulls_temp, float(scale_(brim_width))) :
|
||||
convex_hulls_temp;
|
||||
Polygons tower_polys_estimated;
|
||||
if (!exact_footprint && !convex_hulls_temp.empty()) {
|
||||
double max_height = 0.;
|
||||
for (const PrintObject *object : print.objects())
|
||||
max_height = std::max(max_height, unscale_(object->size().z()));
|
||||
Polygon base = estimate_wipe_tower_first_layer_outline(config, print.wipe_tower_type(), width, depth, max_height);
|
||||
base.rotate(Geometry::deg2rad(a));
|
||||
base.translate(Point(scale_(x), scale_(y)));
|
||||
tower_polys_estimated = offset(base, float(scale_(brim_width)));
|
||||
}
|
||||
// Object proximity stays a body-only warning: brim near-misses would newly warn on
|
||||
// many setups that print fine.
|
||||
if (!intersection(convex_hulls_other, convex_hulls_temp).empty()) {
|
||||
if (warning) {
|
||||
warning->string += L("Prime Tower") + L(" is too close to others, and collisions may be caused.\n");
|
||||
}
|
||||
}
|
||||
if (!intersection(exclude_polys, convex_hulls_temp).empty()) {
|
||||
/*if (warning) {
|
||||
warning->string += L("Prime Tower is too close to exclusion area, there may be collisions when printing.\n");
|
||||
}*/
|
||||
if (!intersection(exclude_polys, tower_polys_checked).empty()) {
|
||||
return {L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n")};
|
||||
}
|
||||
if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, convex_hulls_temp).empty()) {
|
||||
if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_checked).empty()) {
|
||||
return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")};
|
||||
}
|
||||
// Skip the containment check for towers that will never be printed (single-filament
|
||||
// prints without smooth timelapse keep the config's tower position but emit nothing).
|
||||
// Pre-generation only the body square is tested — the auto-brim estimate can overshoot
|
||||
// the generated brim by several mm and must not hard-fail a print that physically fits.
|
||||
// Post-generation the mesh bottom already includes the real brim, so the exact
|
||||
// footprint is tested.
|
||||
if (filaments_count > 1 || print.enable_timelapse_print()) {
|
||||
// The shared printable polygon is plate-local, while the tower polygons above are
|
||||
// already shifted by the plate origin.
|
||||
Polygons printable_polys = print.get_extruder_shared_printable_polygon();
|
||||
const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y()));
|
||||
for (Polygon &p : printable_polys)
|
||||
p.translate(plate_shift);
|
||||
if (!diff(convex_hulls_temp, printable_polys).empty())
|
||||
return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")};
|
||||
if (warning && !intersection(exclude_polys, tower_polys_estimated).empty()) {
|
||||
warning->string += L("Prime Tower") + L(" is too close to exclusion area, there may be collisions when printing.") + "\n";
|
||||
}
|
||||
if (warning && print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_estimated).empty()) {
|
||||
warning->string += L("Prime Tower") + L(" is too close to clumping detection area, there may be collisions when printing.") + "\n";
|
||||
}
|
||||
// No gate on "is there a tower": one that is not printed estimates to zero, so the hulls
|
||||
// are degenerate and every check passes. Re-deriving it here missed the wrapping-detection
|
||||
// tower on a single-filament plate.
|
||||
Polygons printable_polys = print.get_extruder_shared_printable_polygon();
|
||||
const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y()));
|
||||
for (Polygon &p : printable_polys)
|
||||
p.translate(plate_shift);
|
||||
if (!diff(tower_polys_checked, printable_polys).empty())
|
||||
return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")};
|
||||
if (warning && !diff(tower_polys_estimated, printable_polys).empty())
|
||||
warning->string += L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1386,6 +1781,16 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
}
|
||||
if (!layer_warning.string.empty())
|
||||
add_warning(layer_warning);
|
||||
|
||||
// Orca: a compacted prime tower drags the nozzle back down to the plate on every toolchange, so
|
||||
// tall objects collide with it much like they do in sequential printing. Checking it here rather
|
||||
// than only during slicing is what lets the plater show the collision area and the height limit
|
||||
// while the plate is still being arranged.
|
||||
ret = compacted_wipe_tower_clearance_valid(*this, collison_polygons, height_polygons);
|
||||
if (!ret.string.empty()) {
|
||||
ret.type = STRING_EXCEPT_OBJECT_COLLISION_IN_LAYER_PRINT;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_config.enable_prime_tower) {
|
||||
@@ -2598,6 +3003,12 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
|
||||
if (this->has_wipe_tower()) {
|
||||
m_fake_wipe_tower.set_pos({ m_config.wipe_tower_x.get_at(m_plate_index), m_config.wipe_tower_y.get_at(m_plate_index) });
|
||||
// Validated on every process() run rather than only when the wipe tower step is (re)generated.
|
||||
// Moving the tower changes only wipe_tower_x/y, which invalidates psSkirtBrim but not psWipeTower,
|
||||
// so a validate call living inside _make_wipe_tower would be skipped and keep using the stale
|
||||
// position, missing a fresh collision. The tower geometry (tool_changes) is stored in the local
|
||||
// frame and is position independent, so re-checking here with the current position is correct.
|
||||
this->validate_compacted_wipe_tower_clearance();
|
||||
}
|
||||
|
||||
if (this->set_started(psSkirtBrim)) {
|
||||
@@ -3997,74 +4408,25 @@ bool Print::has_wipe_tower() const
|
||||
|
||||
const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
|
||||
{
|
||||
// If the wipe tower wasn't created yet, make sure the depth and brim_width members are set to default.
|
||||
double max_height = 0;
|
||||
for (size_t obj_idx = 0; obj_idx < m_objects.size(); obj_idx++) {
|
||||
double object_z = (double) m_objects[obj_idx]->size().z();
|
||||
max_height = std::max(unscale_(object_z), max_height);
|
||||
// Until the tower is generated, size it with the estimate the GUI/CLI placement uses, so
|
||||
// validation cannot reject a position the clamp just accepted.
|
||||
if (is_step_done(psWipeTower) || filaments_cnt == 0)
|
||||
return m_wipe_tower_data;
|
||||
|
||||
double max_height = 0.;
|
||||
double layer_height = std::numeric_limits<double>::max();
|
||||
for (const PrintObject *object : m_objects) {
|
||||
max_height = std::max(max_height, unscale_(double(object->size().z())));
|
||||
layer_height = std::min(layer_height, object->config().layer_height.value);
|
||||
}
|
||||
if (max_height < EPSILON) return m_wipe_tower_data;
|
||||
if (max_height < EPSILON)
|
||||
return m_wipe_tower_data;
|
||||
|
||||
double layer_height = 0.08f; // hard code layer height
|
||||
layer_height = m_objects.front()->config().layer_height.value;
|
||||
|
||||
auto timelapse_type = config().option<ConfigOptionEnum<TimelapseType>>("timelapse_type");
|
||||
bool need_wipe_tower = (timelapse_type ? (timelapse_type->value == TimelapseType::tlSmooth) : false) | (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib);
|
||||
double extra_spacing = config().option("prime_tower_infill_gap")->getFloat() / 100.;
|
||||
double rib_width = config().option("wipe_tower_rib_width")->getFloat();
|
||||
|
||||
double filament_change_volume = 0.;
|
||||
{
|
||||
std::vector<double> filament_change_lengths;
|
||||
auto filament_change_lengths_opt = config().option<ConfigOptionFloats>("filament_change_length");
|
||||
if (filament_change_lengths_opt) filament_change_lengths = filament_change_lengths_opt->values;
|
||||
double length = filament_change_lengths.empty() ? 0 : *std::max_element(filament_change_lengths.begin(), filament_change_lengths.end());
|
||||
double diameter = 1.75;
|
||||
std::vector<double> diameters;
|
||||
auto filament_diameter_opt = config().option<ConfigOptionFloats>("filament_diameter");
|
||||
if (filament_diameter_opt) diameters = filament_diameter_opt->values;
|
||||
diameter = diameters.empty() ? diameter : *std::max_element(diameters.begin(), diameters.end());
|
||||
filament_change_volume = length * PI * diameter * diameter / 4.;
|
||||
}
|
||||
|
||||
|
||||
if (! is_step_done(psWipeTower) && filaments_cnt !=0) {
|
||||
double wipe_volume = m_config.prime_volume;
|
||||
int filament_depth_count = m_config.nozzle_diameter.values.size() == 2 ? filaments_cnt : filaments_cnt - 1;
|
||||
if (filaments_cnt == 1 && enable_timelapse_print()) filament_depth_count = 1;
|
||||
double volume = wipe_volume * filament_depth_count;
|
||||
if (m_config.nozzle_diameter.values.size() == 2) volume += filament_change_volume * (int) (filaments_cnt / 2);
|
||||
|
||||
// Sizing should take into account currently set wiping volumes.
|
||||
// For a long time, the initial preview would just use 900/width per toolchange (15mm on a 60mm wide tower)
|
||||
// and it worked well enough. Let's try to do slightly better by accounting for the purging volumes.
|
||||
const bool semm_flush = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
|
||||
if (semm_flush) volume = WipeTower2::estimate_semm_flush_volume(m_config, filaments_cnt);
|
||||
|
||||
if (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib) {
|
||||
double depth = std::sqrt(volume / layer_height * extra_spacing);
|
||||
if (need_wipe_tower || filaments_cnt > 1) {
|
||||
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
|
||||
depth = std::max((double) min_wipe_tower_depth, depth);
|
||||
depth += rib_width / std::sqrt(2) + config().wipe_tower_extra_rib_length.value;
|
||||
const_cast<Print *>(this)->m_wipe_tower_data.depth = depth;
|
||||
const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width;
|
||||
}
|
||||
}
|
||||
else {
|
||||
double width = m_config.prime_tower_width;
|
||||
double depth = volume / (layer_height * width);
|
||||
// The flush volumes already hold the spacing between wipes.
|
||||
if (!semm_flush) depth *= extra_spacing;
|
||||
if (need_wipe_tower || depth > EPSILON) {
|
||||
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
|
||||
depth = std::max((double) min_wipe_tower_depth, depth);
|
||||
}
|
||||
const_cast<Print *>(this)->m_wipe_tower_data.depth = depth;
|
||||
const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width;
|
||||
}
|
||||
if (m_config.prime_tower_brim_width < 0) const_cast<Print *>(this)->m_wipe_tower_data.brim_width = WipeTower::get_auto_brim_by_height(max_height);
|
||||
}
|
||||
const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(m_config, this->wipe_tower_type(), this->extruders(true), layer_height, max_height);
|
||||
WipeTowerData &data = const_cast<Print *>(this)->m_wipe_tower_data;
|
||||
data.depth = float(footprint.depth);
|
||||
data.width = float(footprint.width);
|
||||
data.brim_width = float(footprint.brim_width);
|
||||
return m_wipe_tower_data;
|
||||
}
|
||||
|
||||
@@ -4290,6 +4652,7 @@ void Print::_make_wipe_tower()
|
||||
m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size());
|
||||
wipe_tower.generate_new(m_wipe_tower_data.tool_changes);
|
||||
m_wipe_tower_data.depth = wipe_tower.get_depth();
|
||||
m_wipe_tower_data.width = wipe_tower.width();
|
||||
m_wipe_tower_data.brim_width = wipe_tower.get_brim_width();
|
||||
m_wipe_tower_data.bbx = wipe_tower.get_bbx();
|
||||
m_wipe_tower_data.rib_offset = wipe_tower.get_rib_offset();
|
||||
@@ -4403,6 +4766,7 @@ void Print::_make_wipe_tower()
|
||||
m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size());
|
||||
wipe_tower.generate(m_wipe_tower_data.tool_changes);
|
||||
m_wipe_tower_data.depth = wipe_tower.get_depth();
|
||||
m_wipe_tower_data.width = wipe_tower.width();
|
||||
m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs();
|
||||
m_wipe_tower_data.brim_width = wipe_tower.get_brim_width();
|
||||
m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height();
|
||||
@@ -4438,7 +4802,9 @@ void Print::_make_wipe_tower()
|
||||
wipe_tower.get_wipe_tower_height(), wipe_tower.get_brim_width(),
|
||||
config().wipe_tower_wall_type.value == WipeTowerWallType::wtwRib,
|
||||
wipe_tower.get_rib_width(), wipe_tower.get_rib_length(),
|
||||
config().wipe_tower_fillet_wall.value);
|
||||
config().wipe_tower_fillet_wall.value,
|
||||
config().wipe_tower_wall_type.value == WipeTowerWallType::wtwCone ?
|
||||
(float) config().wipe_tower_cone_angle.value : 0.f);
|
||||
const Vec3d origin = Vec3d::Zero();
|
||||
// FakeWipeTower::pos is a bed-frame translation applied after rotation
|
||||
// (getFakeExtrusionPathsFromWipeTower2 rotates about the local origin), so the
|
||||
@@ -4451,6 +4817,28 @@ void Print::_make_wipe_tower()
|
||||
config().wipe_tower_rotation_angle, config().wipe_tower_cone_angle,
|
||||
{scale_(origin.x()), scale_(origin.y())});
|
||||
}
|
||||
|
||||
// The clamps and checks above work from estimates; re-test the exact generated footprint
|
||||
// so an off-plate tower fails with a clear error instead of exporting unprintable G-code
|
||||
// (validate() only sees the mesh on its next run).
|
||||
if (m_wipe_tower_data.wipe_tower_mesh_data) {
|
||||
Polygon footprint = m_wipe_tower_data.wipe_tower_mesh_data->bottom; // includes brim and rib offset
|
||||
footprint.rotate(Geometry::deg2rad(m_config.wipe_tower_rotation_angle.value));
|
||||
footprint.translate(Point(scale_(m_config.wipe_tower_x.get_at(m_plate_index)),
|
||||
scale_(m_config.wipe_tower_y.get_at(m_plate_index))));
|
||||
const Polygons printable_polys = this->get_extruder_shared_printable_polygon();
|
||||
if (!printable_polys.empty() && !diff(Polygons{footprint}, printable_polys).empty()) {
|
||||
const BoundingBox fp = get_extents(footprint);
|
||||
const BoundingBox pr = get_extents(printable_polys);
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("wipe tower footprint [%1%,%2%]-[%3%,%4%] leaves printable [%5%,%6%]-[%7%,%8%]") %
|
||||
unscaled(fp.min.x()) % unscaled(fp.min.y()) % unscaled(fp.max.x()) % unscaled(fp.max.y()) %
|
||||
unscaled(pr.min.x()) % unscaled(pr.min.y()) % unscaled(pr.max.x()) % unscaled(pr.max.y());
|
||||
throw Slic3r::SlicingError(L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n"));
|
||||
}
|
||||
// The cutter/purge corner is a physical obstacle — the brim must stay out like the body.
|
||||
if (!intersection(get_bed_excluded_area(m_config), Polygons{footprint}).empty())
|
||||
throw Slic3r::SlicingError(L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n"));
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a recommended G-code output file name based on the format template, default extension, and template parameters
|
||||
@@ -5999,17 +6387,26 @@ ExtrusionLayers FakeWipeTower::getTrueExtrusionLayersFromWipeTower() const
|
||||
}
|
||||
return wtels;
|
||||
}
|
||||
void WipeTowerData::construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length,bool fillet_wall)
|
||||
void WipeTowerData::construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length,bool fillet_wall, float cone_angle)
|
||||
{
|
||||
wipe_tower_mesh_data = WipeTowerMeshData{};
|
||||
float first_layer_height=0.08; //brim height
|
||||
if (width < EPSILON || depth < EPSILON || height < EPSILON) return;
|
||||
if (!is_rib_wipe_tower || rib_length < EPSILON) {
|
||||
if (cone_angle > EPSILON && (!is_rib_wipe_tower || rib_length < EPSILON)) {
|
||||
// Cone tower: the base bulges past the body box; this bottom polygon feeds the
|
||||
// containment checks, so it must carry the bulge and the brim (cone not lofted).
|
||||
wipe_tower_mesh_data->real_wipe_tower_mesh = make_cube(width, depth, height);
|
||||
wipe_tower_mesh_data->bottom = WipeTower2::cone_base_polygon(width, depth, height, cone_angle);
|
||||
auto brim_bottom = offset(wipe_tower_mesh_data->bottom, scaled(brim_width));
|
||||
if (!brim_bottom.empty())
|
||||
wipe_tower_mesh_data->bottom = brim_bottom.front();
|
||||
wipe_tower_mesh_data->real_brim_mesh = WipeTower::its_make_rib_brim(wipe_tower_mesh_data->bottom, first_layer_height);
|
||||
} else if (!is_rib_wipe_tower || rib_length < EPSILON) {
|
||||
wipe_tower_mesh_data->real_wipe_tower_mesh = make_cube(width, depth, height);
|
||||
wipe_tower_mesh_data->real_brim_mesh = make_cube(width + 2 * brim_width, depth + 2 * brim_width, first_layer_height);
|
||||
wipe_tower_mesh_data->real_brim_mesh.translate({-brim_width, -brim_width, 0});
|
||||
wipe_tower_mesh_data->bottom = {scaled(Vec2f{-brim_width, -brim_width}), scaled(Vec2f{width + brim_width, 0}), scaled(Vec2f{width + brim_width, depth + brim_width}),
|
||||
scaled(Vec2f{0, depth})};
|
||||
wipe_tower_mesh_data->bottom = {scaled(Vec2f{-brim_width, -brim_width}), scaled(Vec2f{width + brim_width, -brim_width}),
|
||||
scaled(Vec2f{width + brim_width, depth + brim_width}), scaled(Vec2f{-brim_width, depth + brim_width})};
|
||||
} else {
|
||||
wipe_tower_mesh_data->real_wipe_tower_mesh = WipeTower::its_make_rib_tower(width, depth, height, rib_length, rib_width, fillet_wall);
|
||||
wipe_tower_mesh_data->bottom = WipeTower::rib_section(width, depth, rib_length, rib_width, fillet_wall);
|
||||
|
||||
+92
-1
@@ -782,6 +782,9 @@ struct WipeTowerData
|
||||
|
||||
// Depth of the wipe tower to pass to GLCanvas3D for exact bounding box:
|
||||
float depth;
|
||||
// Effective width (a rib wall squares the tower): the estimate until generation, then the
|
||||
// generated width, so it never disagrees with depth.
|
||||
float width;
|
||||
std::vector<std::pair<float, float>> z_and_depth_pairs;
|
||||
float brim_width;
|
||||
float height;
|
||||
@@ -795,12 +798,13 @@ struct WipeTowerData
|
||||
used_filament.clear();
|
||||
number_of_toolchanges = -1;
|
||||
depth = 0.f;
|
||||
width = 0.f;
|
||||
brim_width = 0.f;
|
||||
height = 0.f;
|
||||
rib_offset = Vec2f::Zero();
|
||||
wipe_tower_mesh_data = std::nullopt;
|
||||
}
|
||||
void construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length, bool fillet_wall);
|
||||
void construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length, bool fillet_wall, float cone_angle = 0.f);
|
||||
|
||||
private:
|
||||
// Only allow the WipeTowerData to be instantiated internally by Print,
|
||||
@@ -1156,6 +1160,8 @@ public:
|
||||
|
||||
//BBS
|
||||
static StringObjectException sequential_print_clearance_valid(const Print &print, Polygons *polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr);
|
||||
// Orca: pre-slice clearance check for a prime tower compacted by "No sparse layers".
|
||||
static StringObjectException compacted_wipe_tower_clearance_valid(const Print &print, Polygons *polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr);
|
||||
ConflictResultOpt get_conflict_result() const { return m_conflict_result; }
|
||||
|
||||
// Return 4 wipe tower corners in the world coordinates (shifted and rotated), including the wipe tower brim.
|
||||
@@ -1170,6 +1176,8 @@ public:
|
||||
void set_calib_params(const Calib_Params& params);
|
||||
const Calib_Params& calib_params() const { return m_calib_params; }
|
||||
Vec2d translate_to_print_space(const Vec2d &point) const;
|
||||
// Orca: precise counterpart of compacted_wipe_tower_clearance_valid(), run once the tower exists.
|
||||
void validate_compacted_wipe_tower_clearance() const;
|
||||
float get_wipe_tower_depth() const { return m_wipe_tower_data.depth; }
|
||||
BoundingBoxf get_wipe_tower_bbx() const { return m_wipe_tower_data.bbx; }
|
||||
Vec2f get_rib_offset() const { return m_wipe_tower_data.rib_offset; }
|
||||
@@ -1390,6 +1398,89 @@ public:
|
||||
};
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Clearance rule for a prime tower compacted by wipe_tower_no_sparse_layers. Shared by the precise
|
||||
// check that runs on the real extrusions, the pre-slice estimate that feeds the plater with collision
|
||||
// polygons, and the plater's own live preview while the user drags the tower or an object around.
|
||||
// Keeping the rule in one place is what stops those three from drifting apart and reporting different
|
||||
// things for the same plate.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
// Half of a clearance distance, the share each of the two outlines carries. Sequential printing splits
|
||||
// extruder_clearance_radius between the two object hulls this way; the tower checks split their
|
||||
// clearances between the tower ring and the instance hull for the same reason, so that the two
|
||||
// outlines the plater draws touch precisely when the check trips. The 0.2 mm comes off first: it is
|
||||
// the rounding slack the sequential check applies, 0.1 mm per side.
|
||||
inline double compacted_tower_half_clearance(double clearance) { return 0.5 * (clearance - 0.2); }
|
||||
|
||||
// Keep-out geometry a compacted tower projects onto the plate, derived from its bare footprint.
|
||||
struct CompactedTowerZone
|
||||
{
|
||||
// Footprint the checks work on: the raw outline grown by the spiral Z-hop envelope.
|
||||
Polygon hull;
|
||||
// hull grown by half the toolhead radius; an object whose own half-grown hull reaches into it is
|
||||
// hit by the head body. This is also the ring the plater draws.
|
||||
Polygons grown_body;
|
||||
// hull grown by half the bare nozzle cone radius, the innermost tier.
|
||||
Polygons grown_nozzle;
|
||||
// hull bounding box, the Y band the rod sweeps.
|
||||
BoundingBox bbox_rod;
|
||||
// Full body clearance, of which grown_body carries half. Which of the two tiers applies is decided
|
||||
// per object rather than here; see compacted_wipe_tower_clearance().
|
||||
double body_radius { 0. };
|
||||
|
||||
bool empty() const { return hull.points.empty(); }
|
||||
};
|
||||
|
||||
// Per-side padding a bare wipe tower outline needs before the clearance checks may treat it as the
|
||||
// tower's footprint. Callers whose outline already carries the first-layer brim pass zero for it.
|
||||
// Shared by the pre-slice estimate and the plater's live preview: both start from an outline that
|
||||
// falls short of the printed tower in the same two ways, and padding them by different amounts is
|
||||
// exactly how the preview and the validation behind it would end up disagreeing.
|
||||
double compacted_tower_footprint_padding(const PrintConfig &config, double brim_width);
|
||||
|
||||
// Grow a bare tower footprint (bed frame, scaled) into its keep-out zone.
|
||||
CompactedTowerZone compacted_wipe_tower_zone(const PrintConfig &config, const Polygon &tower_footprint);
|
||||
|
||||
// How far an object may rise above the compacted tower base before the toolhead hits it.
|
||||
struct CompactedTowerClearance
|
||||
{
|
||||
// Height the object may reach above the tower base. Zero means it may not rise at all.
|
||||
double allowed_rise;
|
||||
// Clearance that applies once the object stands clear of the toolhead in XY, i.e. rod or lid.
|
||||
double far_clearance;
|
||||
// The object sits within the toolhead radius, so the head body limits it rather than the rod.
|
||||
bool near_body;
|
||||
// Horizontal clearance this particular object has to keep from the tower: the full toolhead
|
||||
// radius once it rises past the nozzle cone, the bare cone while it stays below. It is what the
|
||||
// error message quotes and what the plater grows the object outline by.
|
||||
double body_clearance;
|
||||
};
|
||||
|
||||
// object_rise is the height above the tower base that the caller is going to compare against
|
||||
// allowed_rise. It also selects the horizontal tier, so the two cannot disagree.
|
||||
CompactedTowerClearance compacted_wipe_tower_clearance(const PrintConfig &config, const CompactedTowerZone &zone,
|
||||
const Polygon &inst_hull, double object_rise);
|
||||
|
||||
// This object was judged on a tier reaching past the bare nozzle cone, so the wide ring is the one its
|
||||
// outline has to be drawn against.
|
||||
inline bool compacted_tower_body_tier(const CompactedTowerClearance &clearance)
|
||||
{
|
||||
return clearance.body_clearance > double(MAX_OUTER_NOZZLE_DIAMETER);
|
||||
}
|
||||
|
||||
// Keep-out rings to draw around the tower. The nozzle one always applies; the wide body one is drawn
|
||||
// only when some object on the plate is actually measured against it, otherwise it would show a
|
||||
// keep-out zone no object can violate.
|
||||
Polygons compacted_wipe_tower_rings(const CompactedTowerZone &zone, bool any_body_tier);
|
||||
|
||||
// Outline to hand the plater for an offending object: the instance hull grown by the same half
|
||||
// clearance the check grew it by, which is CompactedTowerClearance::body_clearance for that object.
|
||||
// Sequential printing reports its hulls the same way, and it doubles as the fix for the bare hull
|
||||
// being unusable on screen, where drawn flat it hides under the object and drawn at the height limit
|
||||
// it ends up buried inside the mesh.
|
||||
Polygon compacted_wipe_tower_offender_outline(const Polygon &inst_hull, double body_clearance);
|
||||
|
||||
} /* slic3r_Print_hpp_ */
|
||||
|
||||
#endif
|
||||
|
||||
+185
-181
@@ -2549,6 +2549,16 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_labels.push_back("5");
|
||||
def->mode = comAdvanced;
|
||||
|
||||
// Orca: already carried by the BBL/Qidi/Geeetech/Eryone machine profiles, which inherited it from
|
||||
// the BambuStudio import; without a definition here it was parsed as an unknown key and dropped.
|
||||
def = this->add("extruder_clearance_dist_to_rod", coFloat);
|
||||
def->label = L("Distance to rod");
|
||||
def->tooltip = L("Horizontal distance of the nozzle tip to the rod's farther edge. Used for collision avoidance in by-object printing.");
|
||||
def->sidetext = L("mm"); // millimeters, CIS languages need translation
|
||||
def->min = 0;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(40));
|
||||
|
||||
def = this->add("extruder_clearance_height_to_rod", coFloat);
|
||||
def->label = L("Height to rod");
|
||||
def->tooltip = L("Distance from the nozzle tip to the lower rod. Used for collision avoidance in by-object printing.");
|
||||
@@ -5430,7 +5440,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->readonly = false;
|
||||
def->nullable = true;
|
||||
def->set_default_value(new ConfigOptionFloatsNullable { {0.0} });
|
||||
def->set_default_value(new ConfigOptionFloatsNullable { 0.0 });
|
||||
|
||||
def = this->add("cooling_tube_retraction", coFloat);
|
||||
def->label = L("Cooling tube position");
|
||||
@@ -5537,6 +5547,16 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(true));
|
||||
|
||||
def = this->add("unsupported_wall_last", coBool);
|
||||
def->label = L("Print unsupported walls last");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("Wall loops that lie entirely in mid air are printed once something can hold them:\n"
|
||||
"they are extruded after the other walls of their island, innermost first, whatever the wall order is.\n"
|
||||
"A loop that only the bridges of this layer can anchor waits until those bridges are printed, while a loop running "
|
||||
"alongside a supported wall keeps its place before the infill, which needs it as an anchor.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("outer_wall_filament_id", coInt);
|
||||
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
|
||||
def->label = L("Outer walls");
|
||||
@@ -6267,6 +6287,36 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("wipe_inward", coBool);
|
||||
def->label = L("Wipe inward");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("Applies only to external walls, including hole boundaries. Moves the hot nozzle toward printed "
|
||||
"inner walls during wiping to reduce reheating of freshly printed plastic and seam marks.\n\n"
|
||||
"Especially useful at layer heights below 0.1 mm, where wipe marks are more visible.\n\n"
|
||||
"Uses the regular wipe if no adjacent inner wall is already printed (single-wall areas or "
|
||||
"Outer/Inner wall order), or if no supported inward path can be found, for example at tight "
|
||||
"corners or seam gaps.");
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("wipe_inward_distance", coFloatOrPercent);
|
||||
def->label = L("Wipe inward distance");
|
||||
def->category = L("Quality");
|
||||
// xgettext:no-c-format, no-boost-format
|
||||
def->tooltip = L("The distance the wipe path is shifted away from the external perimeter, specified in millimeters "
|
||||
"or as a percentage of the actual outer-wall extrusion width.\n\n"
|
||||
"For example, 50% shifts the path by half of the outer-wall width. The effective offset is limited "
|
||||
"by both the actual outer-wall width and the available spacing to the adjacent wall, so values "
|
||||
"above 100% or an equivalent absolute distance have no additional effect. "
|
||||
"Set to 0 to disable the offset.");
|
||||
def->sidetext = L("mm or %");
|
||||
def->ratio_over = "outer_wall_line_width";
|
||||
def->min = 0;
|
||||
def->max = 100;
|
||||
def->max_literal = 2; // Orca: G-code generation also clamps literal values to the actual outer-wall width.
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionFloatOrPercent(50, true));
|
||||
|
||||
def = this->add("wipe_before_external_loop", coBool);
|
||||
def->label = L("Wipe before external loop");
|
||||
def->category = L("Quality");
|
||||
@@ -6644,8 +6694,10 @@ void PrintConfigDef::init_fff_params()
|
||||
def = this->add("wipe_tower_no_sparse_layers", coBool);
|
||||
def->label = L("No sparse layers (beta)");
|
||||
def->tooltip = L("If enabled, the wipe tower will not be printed on layers with no tool changes. "
|
||||
"On layers with a tool change, extruder will travel downward to print the wipe tower. "
|
||||
"User is responsible for ensuring there is no collision with the print.");
|
||||
"On layers with a tool change, extruder will travel downward to print the wipe tower, "
|
||||
"so the tower ends up below the model and the toolhead has to reach down to it. "
|
||||
"Layouts where that would collide with an already printed object are rejected. "
|
||||
"Has no effect with smooth timelapse or clumping detection, which need a tower on every layer.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
@@ -6671,6 +6723,34 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_labels.emplace_back(L("Cyclic"));
|
||||
def->set_default_value(new ConfigOptionEnum<ToolChangeOrderingType>(ToolChangeOrderingType::Default));
|
||||
|
||||
def = this->add("toolchange_cyclic_order", coString);
|
||||
def->label = L("Cyclic order");
|
||||
def->category = L("Advanced");
|
||||
def->tooltip = L(
|
||||
"Custom filament sequence used by the cyclic toolchange ordering, as filament numbers separated by commas (e.g. \"3,2,1,4\").\n"
|
||||
"Each layer prints its filaments following this sequence; filaments not listed are printed last, in ascending order.\n"
|
||||
"Leave empty to cycle through the filaments in ascending order."
|
||||
);
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionString(""));
|
||||
|
||||
def = this->add("toolchange_cyclic_first_layer", coBool);
|
||||
def->label = L("Apply cyclic order to first layer");
|
||||
def->category = L("Advanced");
|
||||
def->tooltip = L(
|
||||
"Applies the cyclic toolchange order to the first layer as well.\n"
|
||||
"By default this is disabled, because the first layer is instead ordered for the best bed "
|
||||
"adhesion: filaments that print small, fragile first-layer features are printed last, so the "
|
||||
"following tool changes and travel moves are less likely to knock those weakly anchored parts "
|
||||
"loose. This first-layer order also honors a custom first layer filament sequence when one is set. "
|
||||
"The cyclic order's benefit (extra tool changes give each layer more time to cool) does not apply "
|
||||
"to the first layer, which is printed slowly and hot for adhesion.\n"
|
||||
"Enable this only if you need the exact same tool sequence on every layer, including the first, at "
|
||||
"the cost of that adhesion optimization."
|
||||
);
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("slice_closing_radius", coFloat);
|
||||
def->label = L("Slice gap closing radius");
|
||||
def->category = L("Quality");
|
||||
@@ -6683,7 +6763,7 @@ void PrintConfigDef::init_fff_params()
|
||||
|
||||
def = this->add("slicing_mode", coEnum);
|
||||
def->label = L("Slicing Mode");
|
||||
def->category = L("Other");
|
||||
def->category = L("Others");
|
||||
def->tooltip = L("Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model.");
|
||||
def->enum_keys_map = &ConfigOptionEnum<SlicingMode>::get_enum_values();
|
||||
def->enum_values.push_back("regular");
|
||||
@@ -10936,6 +11016,28 @@ std::vector<int> DynamicPrintConfig::update_values_to_printer_extruders(DynamicP
|
||||
return variant_index;
|
||||
}
|
||||
|
||||
// Regathers a vector option's values through per-slot source indices (one input index per
|
||||
// output slot). Out-of-range indices keep the first value, matching get_at's fallback.
|
||||
template<typename OptType, typename ValueType>
|
||||
static void gather_option_values(const char *caller, const std::string &key, OptType *opt, const std::vector<int> &slot_param_indices)
|
||||
{
|
||||
if (!opt || opt->values.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << caller << boost::format(", Line %1%: option %2% not found or empty, skipping")%__LINE__%key;
|
||||
return;
|
||||
}
|
||||
std::vector<ValueType> new_values;
|
||||
new_values.reserve(slot_param_indices.size());
|
||||
for (int idx : slot_param_indices) {
|
||||
if (idx < 0 || static_cast<size_t>(idx) >= opt->values.size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << caller << boost::format(", Line %1%: option %2% slot index %3% out of range, keeping first value")%__LINE__%key%idx;
|
||||
new_values.emplace_back(opt->values.front());
|
||||
}
|
||||
else
|
||||
new_values.emplace_back(opt->values[idx]);
|
||||
}
|
||||
opt->values = std::move(new_values);
|
||||
}
|
||||
|
||||
void DynamicPrintConfig::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)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: extruder_count %2%, extruder_nozzle_volume_count %3%")%__LINE__ %extruder_count %extruder_nozzle_volume_count;
|
||||
@@ -11013,155 +11115,18 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: can not find opt define for %2%")%__LINE__%key;
|
||||
continue;
|
||||
}
|
||||
// An empty option has no first value to fall back on; give it one registered default per filament.
|
||||
if (auto *vec = dynamic_cast<ConfigOptionVectorBase*>(this->option(key)); vec && vec->empty() && optdef->default_value)
|
||||
vec->resize(filament_count, optdef->default_value.get());
|
||||
|
||||
switch (optdef->type) {
|
||||
case coStrings:
|
||||
{
|
||||
ConfigOptionStrings * opt = this->option<ConfigOptionStrings>(key);
|
||||
if (!opt) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
|
||||
break;
|
||||
}
|
||||
std::vector<std::string> new_values;
|
||||
|
||||
new_values.resize(filament_count);
|
||||
for (int f_index = 0; f_index < filament_count; f_index++)
|
||||
{
|
||||
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
|
||||
continue;
|
||||
}
|
||||
new_values[f_index] = opt->get_at(variant_index[f_index]);
|
||||
}
|
||||
opt->values = new_values;
|
||||
break;
|
||||
}
|
||||
case coInts:
|
||||
{
|
||||
ConfigOptionInts * opt = this->option<ConfigOptionInts>(key);
|
||||
if (!opt) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
|
||||
break;
|
||||
}
|
||||
std::vector<int> new_values;
|
||||
|
||||
new_values.resize(filament_count);
|
||||
for (int f_index = 0; f_index < filament_count; f_index++)
|
||||
{
|
||||
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
|
||||
continue;
|
||||
}
|
||||
new_values[f_index] = opt->get_at(variant_index[f_index]);
|
||||
}
|
||||
opt->values = new_values;
|
||||
break;
|
||||
}
|
||||
case coFloats:
|
||||
{
|
||||
ConfigOptionFloats * opt = this->option<ConfigOptionFloats>(key);
|
||||
if (!opt) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
|
||||
break;
|
||||
}
|
||||
std::vector<double> new_values;
|
||||
|
||||
new_values.resize(filament_count);
|
||||
for (int f_index = 0; f_index < filament_count; f_index++)
|
||||
{
|
||||
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
|
||||
continue;
|
||||
}
|
||||
new_values[f_index] = opt->get_at(variant_index[f_index]);
|
||||
}
|
||||
opt->values = new_values;
|
||||
break;
|
||||
}
|
||||
case coPercents:
|
||||
{
|
||||
ConfigOptionPercents * opt = this->option<ConfigOptionPercents>(key);
|
||||
if (!opt) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
|
||||
break;
|
||||
}
|
||||
std::vector<double> new_values;
|
||||
|
||||
new_values.resize(filament_count);
|
||||
for (int f_index = 0; f_index < filament_count; f_index++)
|
||||
{
|
||||
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
|
||||
continue;
|
||||
}
|
||||
new_values[f_index] = opt->get_at(variant_index[f_index]);
|
||||
}
|
||||
opt->values = new_values;
|
||||
break;
|
||||
}
|
||||
case coFloatsOrPercents:
|
||||
{
|
||||
ConfigOptionFloatsOrPercents * opt = this->option<ConfigOptionFloatsOrPercents>(key);
|
||||
if (!opt) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
|
||||
break;
|
||||
}
|
||||
std::vector<FloatOrPercent> new_values;
|
||||
|
||||
new_values.resize(filament_count);
|
||||
for (int f_index = 0; f_index < filament_count; f_index++)
|
||||
{
|
||||
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
|
||||
continue;
|
||||
}
|
||||
new_values[f_index] = opt->get_at(variant_index[f_index]);
|
||||
}
|
||||
opt->values = new_values;
|
||||
break;
|
||||
}
|
||||
case coBools:
|
||||
{
|
||||
ConfigOptionBools * opt = this->option<ConfigOptionBools>(key);
|
||||
if (!opt) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
|
||||
break;
|
||||
}
|
||||
std::vector<unsigned char> new_values;
|
||||
|
||||
new_values.resize(filament_count);
|
||||
for (int f_index = 0; f_index < filament_count; f_index++)
|
||||
{
|
||||
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
|
||||
continue;
|
||||
}
|
||||
new_values[f_index] = opt->get_at(variant_index[f_index]);
|
||||
}
|
||||
opt->values = new_values;
|
||||
break;
|
||||
}
|
||||
case coEnums:
|
||||
{
|
||||
ConfigOptionEnumsGeneric * opt = this->option<ConfigOptionEnumsGeneric>(key);
|
||||
if (!opt) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
|
||||
break;
|
||||
}
|
||||
std::vector<int> new_values;
|
||||
|
||||
new_values.resize(filament_count);
|
||||
for (int f_index = 0; f_index < filament_count; f_index++)
|
||||
{
|
||||
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
|
||||
continue;
|
||||
}
|
||||
new_values[f_index] = opt->get_at(variant_index[f_index]);
|
||||
}
|
||||
opt->values = new_values;
|
||||
break;
|
||||
}
|
||||
case coStrings: gather_option_values<ConfigOptionStrings, std::string>(__FUNCTION__, key, this->option<ConfigOptionStrings>(key), variant_index); break;
|
||||
case coInts: gather_option_values<ConfigOptionInts, int>(__FUNCTION__, key, this->option<ConfigOptionInts>(key), variant_index); break;
|
||||
case coFloats: gather_option_values<ConfigOptionFloats, double>(__FUNCTION__, key, this->option<ConfigOptionFloats>(key), variant_index); break;
|
||||
case coPercents: gather_option_values<ConfigOptionPercents, double>(__FUNCTION__, key, this->option<ConfigOptionPercents>(key), variant_index); break;
|
||||
case coFloatsOrPercents: gather_option_values<ConfigOptionFloatsOrPercents, FloatOrPercent>(__FUNCTION__, key, this->option<ConfigOptionFloatsOrPercents>(key), variant_index); break;
|
||||
case coBools: gather_option_values<ConfigOptionBools, unsigned char>(__FUNCTION__, key, this->option<ConfigOptionBools>(key), variant_index); break;
|
||||
case coEnums: gather_option_values<ConfigOptionEnumsGeneric, int>(__FUNCTION__, key, this->option<ConfigOptionEnumsGeneric>(key), variant_index); break;
|
||||
default:
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: unsupported option type for %2%")%__LINE__%key;
|
||||
break;
|
||||
@@ -11180,28 +11145,6 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
|
||||
}
|
||||
}
|
||||
|
||||
// Regathers a vector option's values through per-slot source indices (one input index per
|
||||
// output slot). Out-of-range indices keep the first value, matching get_at's fallback.
|
||||
template<typename OptType, typename ValueType>
|
||||
static void gather_option_values(const std::string &key, OptType *opt, const std::vector<int> &slot_param_indices)
|
||||
{
|
||||
if (!opt || opt->values.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found or empty, skipping")%__LINE__%key;
|
||||
return;
|
||||
}
|
||||
std::vector<ValueType> new_values;
|
||||
new_values.reserve(slot_param_indices.size());
|
||||
for (int idx : slot_param_indices) {
|
||||
if (idx < 0 || static_cast<size_t>(idx) >= opt->values.size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% slot index %3% out of range, keeping first value")%__LINE__%key%idx;
|
||||
new_values.emplace_back(opt->values.front());
|
||||
}
|
||||
else
|
||||
new_values.emplace_back(opt->values[idx]);
|
||||
}
|
||||
opt->values = std::move(new_values);
|
||||
}
|
||||
|
||||
void DynamicPrintConfig::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,
|
||||
@@ -11296,13 +11239,13 @@ void DynamicPrintConfig::update_filament_config_values_for_multiple_extruders(Dy
|
||||
continue;
|
||||
}
|
||||
switch (optdef->type) {
|
||||
case coStrings: gather_option_values<ConfigOptionStrings, std::string>(key, this->option<ConfigOptionStrings>(key), slot_param_indices); break;
|
||||
case coInts: gather_option_values<ConfigOptionInts, int>(key, this->option<ConfigOptionInts>(key), slot_param_indices); break;
|
||||
case coFloats: gather_option_values<ConfigOptionFloats, double>(key, this->option<ConfigOptionFloats>(key), slot_param_indices); break;
|
||||
case coPercents: gather_option_values<ConfigOptionPercents, double>(key, this->option<ConfigOptionPercents>(key), slot_param_indices); break;
|
||||
case coFloatsOrPercents: gather_option_values<ConfigOptionFloatsOrPercents, FloatOrPercent>(key, this->option<ConfigOptionFloatsOrPercents>(key), slot_param_indices); break;
|
||||
case coBools: gather_option_values<ConfigOptionBools, unsigned char>(key, this->option<ConfigOptionBools>(key), slot_param_indices); break;
|
||||
case coEnums: gather_option_values<ConfigOptionEnumsGeneric, int>(key, this->option<ConfigOptionEnumsGeneric>(key), slot_param_indices); break;
|
||||
case coStrings: gather_option_values<ConfigOptionStrings, std::string>(__FUNCTION__, key, this->option<ConfigOptionStrings>(key), slot_param_indices); break;
|
||||
case coInts: gather_option_values<ConfigOptionInts, int>(__FUNCTION__, key, this->option<ConfigOptionInts>(key), slot_param_indices); break;
|
||||
case coFloats: gather_option_values<ConfigOptionFloats, double>(__FUNCTION__, key, this->option<ConfigOptionFloats>(key), slot_param_indices); break;
|
||||
case coPercents: gather_option_values<ConfigOptionPercents, double>(__FUNCTION__, key, this->option<ConfigOptionPercents>(key), slot_param_indices); break;
|
||||
case coFloatsOrPercents: gather_option_values<ConfigOptionFloatsOrPercents, FloatOrPercent>(__FUNCTION__, key, this->option<ConfigOptionFloatsOrPercents>(key), slot_param_indices); break;
|
||||
case coBools: gather_option_values<ConfigOptionBools, unsigned char>(__FUNCTION__, key, this->option<ConfigOptionBools>(key), slot_param_indices); break;
|
||||
case coEnums: gather_option_values<ConfigOptionEnumsGeneric, int>(__FUNCTION__, key, this->option<ConfigOptionEnumsGeneric>(key), slot_param_indices); break;
|
||||
default:
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: unsupported option type for %2%")%__LINE__%key;
|
||||
break;
|
||||
@@ -12031,6 +11974,19 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
||||
def->tooltip = L("Do not run any validity checks, such as G-code path conflicts check.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
// --strict turns the non-critical slicing warnings the CLI otherwise only logs into a
|
||||
// failed run, and records strict_mode in result.json so consumers can tell the modes apart.
|
||||
def = this->add("strict", coBool);
|
||||
def->label = L("Strict mode");
|
||||
def->tooltip = L("Exit non-zero when slicing raises a non-critical warning that is "
|
||||
"otherwise only logged, such as a model that needs support while "
|
||||
"support is disabled. Use this in CI or scripted pipelines that should "
|
||||
"never ship a subtly broken slice. Each such warning is also listed "
|
||||
"with a stable class in the `warnings` array of result.json, which is "
|
||||
"written on Linux only. Cannot be combined with --no-check, which skips "
|
||||
"the support check.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("normative_check", coBool);
|
||||
def->label = L("Normative check");
|
||||
def->tooltip = L("Check the normative items.");
|
||||
@@ -12051,9 +12007,29 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
||||
def->tooltip = L("This outputs the model\u2019s information.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("inspect_mesh", coBool);
|
||||
def->label = L("Inspect mesh (JSON to stdout)");
|
||||
def->tooltip = L("Print a JSON summary of each loaded object to stdout, then exit: its bounding boxes and the "
|
||||
"convex hull faces it can be laid on, with their normals, areas and centers. These are the faces "
|
||||
"the --ground-* options choose from. Machine-readable alternative to --info.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
// --inspect-paint \u2014 dump the per-facet enforcer/blocker/extruder/fuzzy
|
||||
// paint state stored on the loaded model (supports, seam, MMU color,
|
||||
// fuzzy-skin) as JSON. Read-only; lets CI / scripted / AI tooling
|
||||
// reason about existing paint on a .3mf without loading the GUI.
|
||||
def = this->add("inspect_paint", coBool);
|
||||
def->label = L("Inspect paint (JSON to stdout)");
|
||||
def->tooltip = L("Print a structured JSON summary of every painted layer "
|
||||
"(supports, seam, MMU color, fuzzy-skin) already stored on "
|
||||
"the loaded model \u2014 per-state facet count, surface area, "
|
||||
"and mesh-local bounding box \u2014 then exit. Machine-readable "
|
||||
"alternative to opening the paint gizmos in the GUI.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("export_settings", coString);
|
||||
def->label = L("Export Settings");
|
||||
def->tooltip = L("This exports settings to a file.");
|
||||
def->tooltip = L("This exports settings to a file. Use - to write them to stdout.");
|
||||
def->cli_params = "settings.json";
|
||||
def->set_default_value(new ConfigOptionString("output.json"));
|
||||
|
||||
@@ -12170,6 +12146,34 @@ CLITransformConfigDef::CLITransformConfigDef()
|
||||
def->sidetext = u8"°"; // degrees, don't need translation
|
||||
def->set_default_value(new ConfigOptionFloat(0));
|
||||
|
||||
// The --ground-* options choose from the faces the "Lay on Face" gizmo offers. Like the other
|
||||
// transforms they run in command-line order, so they see the rotations given before them.
|
||||
def = this->add("ground_largest_face", coBool);
|
||||
def->label = L("Ground largest face");
|
||||
def->tooltip = L("Lay each object on the largest face of its convex hull and drop it onto the bed. Of equally large "
|
||||
"faces, the one already facing down is kept. Objects without a face large enough to rest on are left "
|
||||
"as they are. Transforms run in command-line order, so rotations given before this option are respected. "
|
||||
"--orient 1 runs after all transforms and replaces the orientation.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("ground_face_normal", coString);
|
||||
def->label = L("Ground face by normal");
|
||||
def->tooltip = L("Lay each object on the convex hull face whose outward normal is closest to the direction NX,NY,NZ "
|
||||
"and drop it onto the bed. The direction is in object coordinates, which include the rotations given "
|
||||
"before this option and match the plate axes unless the input file rotates the object. For example, "
|
||||
"1,0,0 stands the object on its +X side. --orient 1 runs after all transforms and replaces the orientation.");
|
||||
def->cli_params = "NX,NY,NZ";
|
||||
def->set_default_value(new ConfigOptionString(""));
|
||||
|
||||
def = this->add("ground_face_point", coString);
|
||||
def->label = L("Ground face at point");
|
||||
def->tooltip = L("Lay each object on the convex hull face that contains the point X,Y,Z and drop it onto the bed. "
|
||||
"The point is in object coordinates, which include the rotations given before this option; "
|
||||
"--inspect-mesh reports face centers in them. Objects without such a face are left as they are, and "
|
||||
"the run fails if no object has one. --orient 1 runs after all transforms and replaces the orientation.");
|
||||
def->cli_params = "X,Y,Z";
|
||||
def->set_default_value(new ConfigOptionString(""));
|
||||
|
||||
def = this->add("scale", coFloat);
|
||||
def->label = L("Scale");
|
||||
def->tooltip = L("Scale the model by a float factor.");
|
||||
|
||||
@@ -1011,41 +1011,46 @@ public: \
|
||||
{ PrintConfigDef::handle_legacy(opt_key, value); }
|
||||
|
||||
#define PRINT_CONFIG_CLASS_ELEMENT_DEFINITION(r, data, elem) BOOST_PP_TUPLE_ELEM(0, elem) BOOST_PP_TUPLE_ELEM(1, elem);
|
||||
#define PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION2(KEY) cache.opt_add(BOOST_PP_STRINGIZE(KEY), base_ptr, this->KEY);
|
||||
#define PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION(r, data, elem) PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION2(BOOST_PP_TUPLE_ELEM(1, elem))
|
||||
#define PRINT_CONFIG_CLASS_ELEMENT_HASH(r, data, elem) boost::hash_combine(seed, BOOST_PP_TUPLE_ELEM(1, elem).hash());
|
||||
#define PRINT_CONFIG_CLASS_ELEMENT_EQUAL(r, data, elem) if (! (BOOST_PP_TUPLE_ELEM(1, elem) == rhs.BOOST_PP_TUPLE_ELEM(1, elem))) return false;
|
||||
#define PRINT_CONFIG_CLASS_ELEMENT_LOWER(r, data, elem) \
|
||||
if (BOOST_PP_TUPLE_ELEM(1, elem) < rhs.BOOST_PP_TUPLE_ELEM(1, elem)) return true; \
|
||||
if (! (BOOST_PP_TUPLE_ELEM(1, elem) == rhs.BOOST_PP_TUPLE_ELEM(1, elem))) return false;
|
||||
#define PRINT_CONFIG_CLASS_ELEMENT_VISIT(r, data, elem) if (! f(BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(1, elem)), this->BOOST_PP_TUPLE_ELEM(1, elem), rhs.BOOST_PP_TUPLE_ELEM(1, elem))) return;
|
||||
// Each option list is expanded into the members and again into for_each_option_pair(), which calls
|
||||
// f(key, this->option, rhs.option) in declaration order and stops when f returns false. hash(),
|
||||
// operator==, operator< and initialize() iterate the options through that visitor.
|
||||
#define PRINT_CONFIG_CLASS_COMMON_BODY(CLASS_NAME) \
|
||||
size_t hash() const throw() \
|
||||
{ \
|
||||
size_t seed = 0; \
|
||||
this->for_each_option_pair(*this, [&seed](const char*, const auto &a, const auto&) { boost::hash_combine(seed, a.hash()); return true; }); \
|
||||
return seed; \
|
||||
} \
|
||||
bool operator==(const CLASS_NAME &rhs) const throw() \
|
||||
{ \
|
||||
bool eq = true; \
|
||||
this->for_each_option_pair(rhs, [&eq](const char*, const auto &a, const auto &b) { eq = (a == b); return eq; }); \
|
||||
return eq; \
|
||||
} \
|
||||
bool operator!=(const CLASS_NAME &rhs) const throw() { return ! (*this == rhs); } \
|
||||
bool operator<(const CLASS_NAME &rhs) const throw() \
|
||||
{ \
|
||||
int c = 0; \
|
||||
this->for_each_option_pair(rhs, [&c](const char*, const auto &a, const auto &b) { if (a < b) c = -1; else if (! (a == b)) c = 1; return c == 0; }); \
|
||||
return c < 0; \
|
||||
} \
|
||||
protected: \
|
||||
void initialize(StaticCacheBase &cache, const char *base_ptr) \
|
||||
{ \
|
||||
this->for_each_option_pair(*this, [&cache, base_ptr](const char *key, const auto &a, const auto&) { cache.opt_add(key, base_ptr, a); return true; }); \
|
||||
}
|
||||
|
||||
#define PRINT_CONFIG_CLASS_DEFINE(CLASS_NAME, PARAMETER_DEFINITION_SEQ) \
|
||||
class CLASS_NAME : public StaticPrintConfig { \
|
||||
STATIC_PRINT_CONFIG_CACHE(CLASS_NAME) \
|
||||
public: \
|
||||
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_DEFINITION, _, PARAMETER_DEFINITION_SEQ) \
|
||||
size_t hash() const throw() \
|
||||
template<typename F> void for_each_option_pair(const CLASS_NAME &rhs, F &&f) const \
|
||||
{ \
|
||||
size_t seed = 0; \
|
||||
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_HASH, _, PARAMETER_DEFINITION_SEQ) \
|
||||
return seed; \
|
||||
} \
|
||||
bool operator==(const CLASS_NAME &rhs) const throw() \
|
||||
{ \
|
||||
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_EQUAL, _, PARAMETER_DEFINITION_SEQ) \
|
||||
return true; \
|
||||
} \
|
||||
bool operator!=(const CLASS_NAME &rhs) const throw() { return ! (*this == rhs); } \
|
||||
bool operator<(const CLASS_NAME &rhs) const throw() \
|
||||
{ \
|
||||
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_LOWER, _, PARAMETER_DEFINITION_SEQ) \
|
||||
return false; \
|
||||
} \
|
||||
protected: \
|
||||
void initialize(StaticCacheBase &cache, const char *base_ptr) \
|
||||
{ \
|
||||
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION, _, PARAMETER_DEFINITION_SEQ) \
|
||||
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_VISIT, _, PARAMETER_DEFINITION_SEQ) \
|
||||
} \
|
||||
PRINT_CONFIG_CLASS_COMMON_BODY(CLASS_NAME) \
|
||||
};
|
||||
|
||||
#define PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST_ITEM(r, data, i, elem) BOOST_PP_COMMA_IF(i) public elem
|
||||
@@ -1059,43 +1064,43 @@ protected: \
|
||||
if (! (*static_cast<const elem*>(this) == static_cast<const elem&>(rhs))) return false;
|
||||
|
||||
// Generic version, with or without new parameters. Don't use this directly.
|
||||
#define PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, PARAMETER_DEFINITION, PARAMETER_REGISTRATION, PARAMETER_HASHES, PARAMETER_EQUALS) \
|
||||
#define PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, PARAMETER_DEFINITION, PARAMETER_VISIT) \
|
||||
class CLASS_NAME : PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST(CLASSES_PARENTS_TUPLE) { \
|
||||
STATIC_PRINT_CONFIG_CACHE_DERIVED(CLASS_NAME) \
|
||||
CLASS_NAME() : PRINT_CONFIG_CLASS_DERIVED_INITIALIZER(CLASSES_PARENTS_TUPLE, 0) { assert(s_cache_##CLASS_NAME.initialized()); *this = s_cache_##CLASS_NAME.defaults(); } \
|
||||
public: \
|
||||
PARAMETER_DEFINITION \
|
||||
template<typename F> void for_each_option_pair(const CLASS_NAME &rhs, F &&f) const { PARAMETER_VISIT } \
|
||||
size_t hash() const throw() \
|
||||
{ \
|
||||
size_t seed = 0; \
|
||||
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_DERIVED_HASH, _, BOOST_PP_TUPLE_TO_SEQ(CLASSES_PARENTS_TUPLE)) \
|
||||
PARAMETER_HASHES \
|
||||
this->for_each_option_pair(*this, [&seed](const char*, const auto &a, const auto&) { boost::hash_combine(seed, a.hash()); return true; }); \
|
||||
return seed; \
|
||||
} \
|
||||
bool operator==(const CLASS_NAME &rhs) const throw() \
|
||||
{ \
|
||||
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_DERIVED_EQUAL, _, BOOST_PP_TUPLE_TO_SEQ(CLASSES_PARENTS_TUPLE)) \
|
||||
PARAMETER_EQUALS \
|
||||
return true; \
|
||||
bool eq = true; \
|
||||
this->for_each_option_pair(rhs, [&eq](const char*, const auto &a, const auto &b) { eq = (a == b); return eq; }); \
|
||||
return eq; \
|
||||
} \
|
||||
bool operator!=(const CLASS_NAME &rhs) const throw() { return ! (*this == rhs); } \
|
||||
protected: \
|
||||
CLASS_NAME(int) : PRINT_CONFIG_CLASS_DERIVED_INITIALIZER(CLASSES_PARENTS_TUPLE, 1) {} \
|
||||
void initialize(StaticCacheBase &cache, const char* base_ptr) { \
|
||||
PRINT_CONFIG_CLASS_DERIVED_INITCACHE(CLASSES_PARENTS_TUPLE) \
|
||||
PARAMETER_REGISTRATION \
|
||||
this->for_each_option_pair(*this, [&cache, base_ptr](const char *key, const auto &a, const auto&) { cache.opt_add(key, base_ptr, a); return true; }); \
|
||||
} \
|
||||
};
|
||||
// Variant without adding new parameters.
|
||||
#define PRINT_CONFIG_CLASS_DERIVED_DEFINE0(CLASS_NAME, CLASSES_PARENTS_TUPLE) \
|
||||
PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, BOOST_PP_EMPTY(), BOOST_PP_EMPTY(), BOOST_PP_EMPTY(), BOOST_PP_EMPTY())
|
||||
PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, BOOST_PP_EMPTY(), BOOST_PP_EMPTY())
|
||||
// Variant with adding new parameters.
|
||||
#define PRINT_CONFIG_CLASS_DERIVED_DEFINE(CLASS_NAME, CLASSES_PARENTS_TUPLE, PARAMETER_DEFINITION_SEQ) \
|
||||
PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, \
|
||||
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_DEFINITION, _, PARAMETER_DEFINITION_SEQ), \
|
||||
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION, _, PARAMETER_DEFINITION_SEQ), \
|
||||
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_HASH, _, PARAMETER_DEFINITION_SEQ), \
|
||||
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_EQUAL, _, PARAMETER_DEFINITION_SEQ))
|
||||
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_VISIT, _, PARAMETER_DEFINITION_SEQ))
|
||||
|
||||
// This object is mapped to Perl as Slic3r::Config::PrintObject.
|
||||
PRINT_CONFIG_CLASS_DEFINE(
|
||||
@@ -1348,6 +1353,7 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionFloatsNullable, filament_ironing_speed))
|
||||
// Detect bridging perimeters
|
||||
((ConfigOptionBool, detect_overhang_wall))
|
||||
((ConfigOptionBool, unsupported_wall_last))
|
||||
((ConfigOptionInt, outer_wall_filament_id))
|
||||
((ConfigOptionInt, inner_wall_filament_id))
|
||||
((ConfigOptionFloatOrPercent, inner_wall_line_width))
|
||||
@@ -1386,6 +1392,8 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionBool, role_based_wipe_speed))
|
||||
((ConfigOptionFloatOrPercent, wipe_speed))
|
||||
((ConfigOptionBool, wipe_on_loops))
|
||||
((ConfigOptionBool, wipe_inward))
|
||||
((ConfigOptionFloatOrPercent, wipe_inward_distance))
|
||||
((ConfigOptionBool, wipe_before_external_loop))
|
||||
((ConfigOptionEnum<WallInfillOrder>, wall_infill_order))
|
||||
((ConfigOptionBool, precise_outer_wall))
|
||||
@@ -1620,6 +1628,8 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionBool, manual_filament_change))
|
||||
((ConfigOptionBool, single_extruder_multi_material_priming))
|
||||
((ConfigOptionEnum<ToolChangeOrderingType>, toolchange_ordering))
|
||||
((ConfigOptionString, toolchange_cyclic_order))
|
||||
((ConfigOptionBool, toolchange_cyclic_first_layer))
|
||||
((ConfigOptionBool, wipe_tower_no_sparse_layers))
|
||||
((ConfigOptionString, change_filament_gcode))
|
||||
((ConfigOptionString, change_extrusion_role_gcode))
|
||||
@@ -1781,6 +1791,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
|
||||
((ConfigOptionBools, slow_down_for_layer_cooling))
|
||||
((ConfigOptionInts, close_fan_the_first_x_layers))
|
||||
((ConfigOptionEnum<DraftShield>, draft_shield))
|
||||
((ConfigOptionFloat, extruder_clearance_dist_to_rod))//BBS
|
||||
((ConfigOptionFloat, extruder_clearance_height_to_rod))//BBs
|
||||
((ConfigOptionFloat, extruder_clearance_height_to_lid))//BBS
|
||||
((ConfigOptionFloat, extruder_clearance_radius))
|
||||
@@ -2148,11 +2159,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE0(
|
||||
#undef STATIC_PRINT_CONFIG_CACHE_BASE
|
||||
#undef STATIC_PRINT_CONFIG_CACHE_DERIVED
|
||||
#undef PRINT_CONFIG_CLASS_ELEMENT_DEFINITION
|
||||
#undef PRINT_CONFIG_CLASS_ELEMENT_EQUAL
|
||||
#undef PRINT_CONFIG_CLASS_ELEMENT_LOWER
|
||||
#undef PRINT_CONFIG_CLASS_ELEMENT_HASH
|
||||
#undef PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION
|
||||
#undef PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION2
|
||||
#undef PRINT_CONFIG_CLASS_ELEMENT_VISIT
|
||||
#undef PRINT_CONFIG_CLASS_COMMON_BODY
|
||||
#undef PRINT_CONFIG_CLASS_DEFINE
|
||||
#undef PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST
|
||||
#undef PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST_ITEM
|
||||
|
||||
+195
-126
@@ -21,9 +21,11 @@
|
||||
#include "TriangleMeshSlicer.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "Fill/FillAdaptive.hpp"
|
||||
#include "Fill/Fill.hpp"
|
||||
#include "Fill/FillLightning.hpp"
|
||||
#include "Format/STL.hpp"
|
||||
#include "format.hpp"
|
||||
#include "AABBTreeIndirect.hpp"
|
||||
#include "AABBTreeLines.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
@@ -672,6 +674,98 @@ void PrintObject::prepare_infill()
|
||||
} // for each region
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
|
||||
// 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. Compute this before bridges so anchors and extrusion share the same origin.
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Orca: 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); // Orca: 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];
|
||||
// Orca: 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; };
|
||||
// Orca: Index the smaller of two consecutive layers instead of scanning every
|
||||
// pair of islands. The tree prunes distant boxes on fragmented models; exact
|
||||
// polygon intersections still decide connectivity for the remaining candidates.
|
||||
for (size_t i = 0; i + 1 < nl; ++ i) {
|
||||
m_print->throw_if_canceled();
|
||||
size_t layer_a = i, layer_b = i + 1;
|
||||
if (m_layers[layer_a]->lslices.size() < m_layers[layer_b]->lslices.size())
|
||||
std::swap(layer_a, layer_b);
|
||||
const Layer *la = m_layers[layer_a], *lb = m_layers[layer_b];
|
||||
if (lb->lslices.empty())
|
||||
continue;
|
||||
|
||||
using IslandTree = AABBTreeIndirect::Tree<2, coord_t>;
|
||||
std::vector<AABBTreeIndirect::BoundingBoxWrapper> bboxes;
|
||||
bboxes.reserve(lb->lslices.size());
|
||||
for (size_t b = 0; b < lb->lslices.size(); ++ b)
|
||||
bboxes.emplace_back(b, lb->lslices_bboxes[b]);
|
||||
IslandTree tree;
|
||||
tree.build_modify_input(bboxes);
|
||||
for (size_t a = 0; a < la->lslices.size(); ++ a) {
|
||||
const IslandTree::BoundingBox query(la->lslices_bboxes[a].min, la->lslices_bboxes[a].max);
|
||||
AABBTreeIndirect::traverse(tree,
|
||||
[&query](const IslandTree::Node &node) { return node.bbox.intersects(query); },
|
||||
[&](const IslandTree::Node &node) {
|
||||
const size_t b = node.idx;
|
||||
// Orca: Tree boxes include an epsilon, so retain the original box
|
||||
// filter. Already-connected islands cannot change the partition
|
||||
// and need no further polygon intersection.
|
||||
if (la->lslices_bboxes[a].overlap(lb->lslices_bboxes[b]) &&
|
||||
find(offset[layer_a] + a) != find(offset[layer_b] + b) &&
|
||||
! intersection_ex(la->lslices[a], lb->lslices[b]).empty())
|
||||
unite(offset[layer_a] + a, offset[layer_b] + b);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
// Orca: 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]);
|
||||
// Orca: 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)];
|
||||
}
|
||||
}
|
||||
|
||||
// the following step needs to be done before combination because it may need
|
||||
// to remove only half of the combined infill
|
||||
this->bridge_over_infill();
|
||||
@@ -706,71 +800,6 @@ 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;
|
||||
|
||||
@@ -1401,8 +1430,6 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "infill_anchor_max"
|
||||
|| opt_key == "top_surface_line_width"
|
||||
|| 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"
|
||||
@@ -1410,6 +1437,10 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "infill_overhang_angle") {
|
||||
steps.emplace_back(posInfill);
|
||||
} else if (opt_key == "sparse_infill_pattern"
|
||||
// Orca: Body centering now also determines bridge anchors during preparation.
|
||||
// Invalidating preparation also invalidates infill, including top/bottom surfaces.
|
||||
|| opt_key == "center_of_surface_pattern"
|
||||
|| opt_key == "separated_infills"
|
||||
|| opt_key == "sparse_infill_smooth_factor"
|
||||
|| opt_key == "symmetric_infill_y_axis"
|
||||
|| opt_key == "infill_shift_step"
|
||||
@@ -1470,6 +1501,7 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "fuzzy_skin_octaves"
|
||||
|| opt_key == "fuzzy_skin_persistence"
|
||||
|| opt_key == "detect_overhang_wall"
|
||||
|| opt_key == "unsupported_wall_last"
|
||||
|| opt_key == "overhang_reverse"
|
||||
|| opt_key == "overhang_reverse_internal_only"
|
||||
|| opt_key == "overhang_reverse_threshold"
|
||||
@@ -1480,13 +1512,9 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
steps.emplace_back(posPerimeters);
|
||||
steps.emplace_back(posSupportMaterial);
|
||||
} else if (opt_key == "bridge_flow" || opt_key == "internal_bridge_flow") {
|
||||
if (m_config.support_top_z_distance > 0.) {
|
||||
// Only invalidate due to bridging if bridging is enabled.
|
||||
// If later "support_top_z_distance" is modified, the complete PrintObject is invalidated anyway.
|
||||
steps.emplace_back(posPerimeters);
|
||||
steps.emplace_back(posInfill);
|
||||
steps.emplace_back(posSupportMaterial);
|
||||
}
|
||||
steps.emplace_back(posPerimeters);
|
||||
steps.emplace_back(posInfill);
|
||||
steps.emplace_back(posSupportMaterial);
|
||||
} else if (
|
||||
opt_key == "wall_generator"
|
||||
|| opt_key == "wall_transition_length"
|
||||
@@ -1547,6 +1575,8 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "brim_flow_ratio"
|
||||
|| opt_key == "filament_flow_ratio"
|
||||
|| opt_key == "scarf_joint_flow_ratio"
|
||||
|| opt_key == "wipe_inward"
|
||||
|| opt_key == "wipe_inward_distance"
|
||||
|| opt_key == "spiral_starting_flow_ratio"
|
||||
|| opt_key == "spiral_finishing_flow_ratio") {
|
||||
invalidated |= m_print->invalidate_step(psGCodeExport);
|
||||
@@ -1604,7 +1634,9 @@ bool PrintObject::invalidate_step(PrintObjectStep step)
|
||||
bool PrintObject::invalidate_all_steps()
|
||||
{
|
||||
// First call the "invalidate" functions, which may cancel background processing.
|
||||
bool result = Inherited::invalidate_all_steps() | m_print->invalidate_all_steps();
|
||||
const bool inherited_invalidated = Inherited::invalidate_all_steps();
|
||||
const bool print_invalidated = m_print->invalidate_all_steps();
|
||||
bool result = inherited_invalidated || print_invalidated;
|
||||
// Then reset some of the depending values.
|
||||
m_slicing_params.valid = false;
|
||||
return result;
|
||||
@@ -3009,21 +3041,12 @@ void PrintObject::bridge_over_infill()
|
||||
return diff(layers_sparse_infill, not_sparse_infill);
|
||||
};
|
||||
|
||||
// LAMBDA do determine optimal bridging angle
|
||||
auto determine_bridging_angle = [](const Polygons &bridged_area, const Lines &anchors, InfillPattern dominant_pattern, double infill_direction) {
|
||||
// Orca: Derive the fallback bridge direction from the supplied anchor geometry.
|
||||
// Pattern-specific angle selection belongs at the call site, where the supporting
|
||||
// layer and region are known; this helper must not override it with a base config angle.
|
||||
auto determine_bridging_angle = [](const Polygons &bridged_area, const Lines &anchors) {
|
||||
AABBTreeLines::LinesDistancer<Line> lines_tree(anchors);
|
||||
|
||||
// Orca: since 3D Honeycomb was "fixed" by forcing coordf_t layerHeight = scale_(1.0), this is no longer needed.
|
||||
// CorssHatch also does not need fixed angle.
|
||||
//
|
||||
// Check it the infill that require a fixed infill angle.
|
||||
//switch (dominant_pattern) {
|
||||
//case ip3DHoneycomb:
|
||||
//case ipCrossHatch:
|
||||
// return (infill_direction + 45.0) * 2.0 * M_PI / 360.;
|
||||
//default: break;
|
||||
//}
|
||||
|
||||
std::map<double, int> counted_directions;
|
||||
for (const Polygon &p : bridged_area) {
|
||||
double acc_distance = 0;
|
||||
@@ -3089,18 +3112,15 @@ void PrintObject::bridge_over_infill()
|
||||
if (bridging_angle == 0) {
|
||||
bridging_angle = 0.001;
|
||||
}
|
||||
switch (dominant_pattern) {
|
||||
case ipHilbertCurve: bridging_angle += 0.25 * PI; break;
|
||||
case ipOctagramSpiral: bridging_angle += (1.0 / 16.0) * PI; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
return bridging_angle;
|
||||
};
|
||||
|
||||
// LAMBDA that will fill given polygons with lines, exapand the lines to the nearest anchor, and reconstruct polygons from the newly
|
||||
// generated lines
|
||||
auto construct_anchored_polygon = [](Polygons bridged_area, Lines anchors, const Flow &bridging_flow, double bridging_angle) {
|
||||
// Orca: Extend scan sections to the nearest anchors and reconstruct the bridge area.
|
||||
// scan_spacing controls boundary sampling independently of the extrusion spacing;
|
||||
// anchoring overlap and smoothing thresholds still use the physical bridging flow.
|
||||
auto construct_anchored_polygon = [](Polygons bridged_area, Lines anchors, const Flow &bridging_flow, double bridging_angle,
|
||||
coord_t scan_spacing, bool restore_anchors = false) {
|
||||
auto lines_rotate = [](Lines &lines, double cos_angle, double sin_angle) {
|
||||
for (Line &l : lines) {
|
||||
double ax = double(l.a.x());
|
||||
@@ -3127,12 +3147,12 @@ void PrintObject::bridge_over_infill()
|
||||
BoundingBox bb_x = get_extents(bridged_area);
|
||||
BoundingBox bb_y = get_extents(anchors);
|
||||
|
||||
const size_t n_vlines = (bb_x.max.x() - bb_x.min.x() + bridging_flow.scaled_spacing() - 1) / bridging_flow.scaled_spacing();
|
||||
const size_t n_vlines = (bb_x.max.x() - bb_x.min.x() + scan_spacing - 1) / scan_spacing;
|
||||
std::vector<Line> vertical_lines(n_vlines);
|
||||
for (size_t i = 0; i < n_vlines; i++) {
|
||||
// Orca: Make sure the line is placed in the middle of the extrusion
|
||||
// coord_t x = bb_x.min.x() + i * bridging_flow.scaled_spacing();
|
||||
coord_t x = bb_x.min.x() + (i + 0.5) * bridging_flow.scaled_spacing();
|
||||
// Orca: Sample the center of each reconstructed strip. Its edges lie
|
||||
// half a scan step away, even when the sampling is finer than extrusion.
|
||||
coord_t x = bb_x.min.x() + (i + 0.5) * scan_spacing;
|
||||
coord_t y_min = bb_y.min.y() - bridging_flow.scaled_spacing();
|
||||
coord_t y_max = bb_y.max.y() + bridging_flow.scaled_spacing();
|
||||
vertical_lines[i].a = Point{x, y_min};
|
||||
@@ -3155,7 +3175,11 @@ void PrintObject::bridge_over_infill()
|
||||
auto anchors_intersections = anchors_and_walls_tree.intersections_with_line<true>(vertical_lines[i]);
|
||||
|
||||
for (Line §ion : polygon_sections[i]) {
|
||||
auto maybe_below_anchor = std::upper_bound(anchors_intersections.rbegin(), anchors_intersections.rend(), section.a,
|
||||
// Orca: A repaired boundary may already overlap its anchor by one flow width.
|
||||
// Include that overlap in the search so restoring rounded corners does not
|
||||
// extend every already anchored section into the next sparse infill cell.
|
||||
const coord_t overlap = restore_anchors ? bridging_flow.scaled_width() + SCALED_EPSILON : 0;
|
||||
auto maybe_below_anchor = std::upper_bound(anchors_intersections.rbegin(), anchors_intersections.rend(), section.a + Point{0, overlap},
|
||||
[](const Point &a, const std::pair<Point, size_t> &b) {
|
||||
return a.y() > b.first.y();
|
||||
});
|
||||
@@ -3164,7 +3188,7 @@ void PrintObject::bridge_over_infill()
|
||||
section.a.y() -= bridging_flow.scaled_width() * (0.5 + 0.5);
|
||||
}
|
||||
|
||||
auto maybe_upper_anchor = std::upper_bound(anchors_intersections.begin(), anchors_intersections.end(), section.b,
|
||||
auto maybe_upper_anchor = std::upper_bound(anchors_intersections.begin(), anchors_intersections.end(), section.b - Point{0, overlap},
|
||||
[](const Point &a, const std::pair<Point, size_t> &b) {
|
||||
return a.y() < b.first.y();
|
||||
});
|
||||
@@ -3194,7 +3218,9 @@ void PrintObject::bridge_over_infill()
|
||||
});
|
||||
}
|
||||
|
||||
// reconstruct polygon from polygon sections
|
||||
// Orca: Reconstruct the polygon from scan sections. At discontinuities and
|
||||
// strip starts/ends, use half the scan step for the X offsets; using half an
|
||||
// extrusion spacing would overlap the finer strips and distort curved anchors.
|
||||
struct TracedPoly
|
||||
{
|
||||
Points lows;
|
||||
@@ -3220,8 +3246,8 @@ void PrintObject::bridge_over_infill()
|
||||
36.0 * double(bridging_flow.scaled_spacing()) * bridging_flow.scaled_spacing()) {
|
||||
traced_poly.lows.push_back(candidate->a);
|
||||
} else {
|
||||
traced_poly.lows.push_back(traced_poly.lows.back() + Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.lows.push_back(candidate->a - Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.lows.push_back(traced_poly.lows.back() + Point{scan_spacing / 2, 0});
|
||||
traced_poly.lows.push_back(candidate->a - Point{scan_spacing / 2, 0});
|
||||
traced_poly.lows.push_back(candidate->a);
|
||||
}
|
||||
|
||||
@@ -3229,8 +3255,8 @@ void PrintObject::bridge_over_infill()
|
||||
36.0 * double(bridging_flow.scaled_spacing()) * bridging_flow.scaled_spacing()) {
|
||||
traced_poly.highs.push_back(candidate->b);
|
||||
} else {
|
||||
traced_poly.highs.push_back(traced_poly.highs.back() + Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.highs.push_back(candidate->b - Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.highs.push_back(traced_poly.highs.back() + Point{scan_spacing / 2, 0});
|
||||
traced_poly.highs.push_back(candidate->b - Point{scan_spacing / 2, 0});
|
||||
traced_poly.highs.push_back(candidate->b);
|
||||
}
|
||||
segment_added = true;
|
||||
@@ -3238,9 +3264,9 @@ void PrintObject::bridge_over_infill()
|
||||
}
|
||||
|
||||
if (!segment_added) {
|
||||
// Zero overlapping segments, we just close this polygon
|
||||
traced_poly.lows.push_back(traced_poly.lows.back() + Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.highs.push_back(traced_poly.highs.back() + Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
// Orca: No section continues this strip; close at its right edge.
|
||||
traced_poly.lows.push_back(traced_poly.lows.back() + Point{scan_spacing / 2, 0});
|
||||
traced_poly.highs.push_back(traced_poly.highs.back() + Point{scan_spacing / 2, 0});
|
||||
Polygon &new_poly = expanded_bridged_area.emplace_back(std::move(traced_poly.lows));
|
||||
new_poly.points.insert(new_poly.points.end(), traced_poly.highs.rbegin(), traced_poly.highs.rend());
|
||||
traced_poly.lows.clear();
|
||||
@@ -3255,9 +3281,9 @@ void PrintObject::bridge_over_infill()
|
||||
for (const auto &segment : polygon_slice) {
|
||||
if (used_segments.find(&segment) == used_segments.end()) {
|
||||
TracedPoly &new_tp = current_traced_polys.emplace_back();
|
||||
new_tp.lows.push_back(segment.a - Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
new_tp.lows.push_back(segment.a - Point{scan_spacing / 2, 0});
|
||||
new_tp.lows.push_back(segment.a);
|
||||
new_tp.highs.push_back(segment.b - Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
new_tp.highs.push_back(segment.b - Point{scan_spacing / 2, 0});
|
||||
new_tp.highs.push_back(segment.b);
|
||||
}
|
||||
}
|
||||
@@ -3364,7 +3390,10 @@ void PrintObject::bridge_over_infill()
|
||||
total_fill_area = closing(total_fill_area, float(SCALED_EPSILON));
|
||||
expansion_area = closing(expansion_area, float(SCALED_EPSILON));
|
||||
expansion_area = intersection(expansion_area, deep_infill_area);
|
||||
Polylines anchors = intersection_pl(infill_lines[lidx - 1], shrink(expansion_area, spacing));
|
||||
// Orca: Preserve the real lower-layer anchors for every candidate in this
|
||||
// layer. Replacing this shared set for one pattern also changes later regions,
|
||||
// and synthetic straight lines can claim support where no infill is printed.
|
||||
const Polylines anchors = intersection_pl(infill_lines[lidx - 1], shrink(expansion_area, spacing));
|
||||
Polygons internal_unsupported_area = shrink(deep_infill_area, spacing * 4.5);
|
||||
|
||||
#ifdef DEBUG_BRIDGE_OVER_INFILL
|
||||
@@ -3375,6 +3404,9 @@ void PrintObject::bridge_over_infill()
|
||||
std::vector<CandidateSurface> expanded_surfaces;
|
||||
expanded_surfaces.reserve(surfaces_by_layer[lidx].size());
|
||||
for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) {
|
||||
const auto ®ion_config = candidate.region->region().config();
|
||||
const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve ||
|
||||
region_config.sparse_infill_pattern == ipOctagramSpiral;
|
||||
const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true);
|
||||
Polygons area_to_be_bridge = expand(candidate.new_polys, flow.scaled_spacing());
|
||||
area_to_be_bridge = intersection(area_to_be_bridge, deep_infill_area);
|
||||
@@ -3403,20 +3435,40 @@ void PrintObject::bridge_over_infill()
|
||||
to_lines(area_to_be_bridge), to_lines(boundary_plines), to_lines(anchors), to_lines(expansion_area));
|
||||
#endif
|
||||
|
||||
double bridging_angle = 0;
|
||||
if (!anchors.empty()) {
|
||||
bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(anchors),
|
||||
candidate.region->region().config().sparse_infill_pattern.value,
|
||||
candidate.region->region().config().infill_direction.value);
|
||||
} else {
|
||||
// use expansion boundaries as anchors.
|
||||
// Also, use Infill pattern that is neutral for angle determination, since there are no infill lines.
|
||||
bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(boundary_plines), InfillPattern::ipLine, 0);
|
||||
double bridging_angle = -1.;
|
||||
if (!anchors.empty() && turning_pattern) {
|
||||
// Orca: Keep adjacent bridges over Hilbert/Octagram aligned despite
|
||||
// their many local turning directions. Use the lower layer's rotation,
|
||||
// since that is the infill supporting the bridge, not the current layer's.
|
||||
for (const LayerRegion *lower_region : layer->lower_layer->regions()) {
|
||||
// Orca: Apply the configured direction only if the same region has
|
||||
// sparse infill below this bridge. A height modifier may put another
|
||||
// pattern underneath, requiring the geometry-based fallback below.
|
||||
if (&lower_region->region() != &candidate.region->region() ||
|
||||
intersection(area_to_be_bridge, to_polygons(lower_region->fill_surfaces.filter_by_type(stInternal))).empty())
|
||||
continue;
|
||||
bridging_angle = calculate_infill_rotation_angle(po, layer->lower_layer->id(), region_config.infill_direction.value,
|
||||
region_config.sparse_infill_rotate_template.value) + 0.5 * PI;
|
||||
// Orca: Apply model alignment as infill generation does, then normalize
|
||||
// the undirected bridge angle to [0, PI), including negative rotations.
|
||||
if (region_config.align_infill_direction_to_model) {
|
||||
const auto &m = po->trafo().matrix();
|
||||
bridging_angle += std::atan2(double(m(1, 0)), double(m(0, 0)));
|
||||
}
|
||||
bridging_angle = std::fmod(bridging_angle, PI);
|
||||
if (bridging_angle < 0.)
|
||||
bridging_angle += PI;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Orca: A different region below (e.g. a height modifier) needs the actual anchor
|
||||
// directions. When there are no sparse anchors, use the expansion boundaries.
|
||||
if (bridging_angle < 0.)
|
||||
bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(anchors.empty() ? boundary_plines : anchors));
|
||||
|
||||
// ORCA: Internal bridge angle override
|
||||
// Orca: Preserve the user's absolute or relative internal bridge angle
|
||||
// override after automatic direction selection.
|
||||
if (candidate.region->region().config().internal_bridge_angle.value > 0) {
|
||||
const auto ®ion_config = candidate.region->region().config();
|
||||
const double custom_angle_rad = Geometry::deg2rad(region_config.internal_bridge_angle.value);
|
||||
if (region_config.relative_bridge_angle.value)
|
||||
bridging_angle += custom_angle_rad;
|
||||
@@ -3429,11 +3481,19 @@ void PrintObject::bridge_over_infill()
|
||||
}
|
||||
}
|
||||
|
||||
// Orca: Changing the bridge direction must not change its physical supports.
|
||||
// Extend to actual sparse infill or the existing boundary anchors, never to
|
||||
// a synthetic grid that merely has the same nominal angle and spacing.
|
||||
boundary_plines.insert(boundary_plines.end(), anchors.begin(), anchors.end());
|
||||
if (!lightning_area.empty() && !intersection(area_to_be_bridge, lightning_area).empty()) {
|
||||
boundary_plines = intersection_pl(boundary_plines, expand(area_to_be_bridge, scale_(10)));
|
||||
}
|
||||
Polygons bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle);
|
||||
// Orca: Use four samples per extrusion spacing for Hilbert/Octagram so the
|
||||
// reconstructed boundary follows rounded anchors instead of cutting corners.
|
||||
// Keep the original step for other patterns and at least one coordinate unit
|
||||
// after integer division. This changes boundary accuracy, not infill density.
|
||||
const coord_t scan_spacing = std::max(coord_t(1), flow.scaled_spacing() / (turning_pattern ? 4 : 1));
|
||||
Polygons bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle, scan_spacing);
|
||||
|
||||
// Check collision with other expanded surfaces
|
||||
{
|
||||
@@ -3447,7 +3507,9 @@ void PrintObject::bridge_over_infill()
|
||||
}
|
||||
}
|
||||
if (reconstruct) {
|
||||
bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle);
|
||||
// Orca: Retain the same sampling accuracy when matching a nearby
|
||||
// bridge's direction; rebuilding must not lose the curved supports.
|
||||
bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle, scan_spacing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3455,6 +3517,13 @@ void PrintObject::bridge_over_infill()
|
||||
// bridging_area = opening(bridging_area, flow.scaled_spacing());
|
||||
bridging_area = opening(bridging_area, flow.scaled_spacing() * 0.75);
|
||||
bridging_area = closing(bridging_area, flow.scaled_spacing());
|
||||
// Orca: Opening/closing can pull rounded bridge ends away from their real
|
||||
// supports. Restore those contacts after smoothing, preserving the cleaned
|
||||
// area and the selected angle; do not smooth the restored contacts again.
|
||||
if (turning_pattern && !bridging_area.empty()) {
|
||||
bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow,
|
||||
bridging_angle, scan_spacing, true));
|
||||
}
|
||||
bridging_area = intersection(bridging_area, limiting_area);
|
||||
bridging_area = intersection(bridging_area, total_fill_area);
|
||||
bridging_area = diff(bridging_area, total_top_area);
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
#include "PublishSettings.hpp"
|
||||
|
||||
#include "PresetBundle.hpp"
|
||||
#include "Preset.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "MaterialType.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
std::string publish_base_key(const std::string &key)
|
||||
{
|
||||
const size_t pos = key.find('#');
|
||||
return pos == std::string::npos ? key : key.substr(0, pos);
|
||||
}
|
||||
|
||||
// Parse the trailing "#N" variant index ("retraction_length#2" -> 2). Returns -1 when the key
|
||||
// carries no '#' separator or its suffix is malformed; mirrors the importer's strict parse
|
||||
// (PresetBundle.cpp) so the export side rejects the same variants the receiver would skip.
|
||||
static int publish_variant_index(const std::string &key, const std::string &base_key)
|
||||
{
|
||||
if (key.size() <= base_key.size() || key.compare(0, base_key.size(), base_key) != 0 || key[base_key.size()] != '#')
|
||||
return -1;
|
||||
const std::string suffix = key.substr(base_key.size() + 1);
|
||||
if (suffix.empty())
|
||||
return -1;
|
||||
int idx = 0;
|
||||
for (const char c : suffix) {
|
||||
if (c < '0' || c > '9')
|
||||
return -1;
|
||||
idx = idx * 10 + (c - '0');
|
||||
if (idx > 1000000) // overflow guard; real vector sizes are tiny
|
||||
return -1;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
std::string normalize_filament_type(const std::string& type)
|
||||
{
|
||||
if (type.empty())
|
||||
return type;
|
||||
if (MaterialType::find(type) != nullptr)
|
||||
return type;
|
||||
// "PLA High Speed" -> "PLA": strip a space-separated modifier, but keep dash-separated
|
||||
// types like "PA-CF" / "PETG-CF" intact (they are distinct materials, not modifiers).
|
||||
const size_t sep = type.find(' ');
|
||||
if (sep != std::string::npos) {
|
||||
const std::string base = type.substr(0, sep);
|
||||
if (MaterialType::find(base) != nullptr)
|
||||
return base;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
void make_publish_universal(DynamicPrintConfig &config)
|
||||
{
|
||||
// Lists: empty => compatible with every printer / every print preset. Conditions:
|
||||
// empty so a leftover expression left behind by the baseline clone can never
|
||||
// re-narrow the match (see is_compatible_with_printer, Preset.cpp:840). All four
|
||||
// keys exist on filament presets; nil-guard for hand-crafted future schemas.
|
||||
if (auto *opt = config.opt<ConfigOptionStrings>("compatible_printers", false))
|
||||
opt->values.clear();
|
||||
if (auto *opt = config.opt<ConfigOptionStrings>("compatible_prints", false))
|
||||
opt->values.clear();
|
||||
if (auto *opt = config.opt<ConfigOptionString>("compatible_printers_condition", false))
|
||||
opt->value.clear();
|
||||
if (auto *opt = config.opt<ConfigOptionString>("compatible_prints_condition", false))
|
||||
opt->value.clear();
|
||||
}
|
||||
|
||||
std::string publish_material_base_name(const std::string &preset_name)
|
||||
{
|
||||
if (preset_name.empty())
|
||||
return preset_name;
|
||||
const size_t at = preset_name.find('@');
|
||||
std::string base = (at == std::string::npos) ? preset_name : preset_name.substr(0, at);
|
||||
boost::trim_right(base);
|
||||
return base;
|
||||
}
|
||||
|
||||
const std::set<std::string>& publish_structural_keys()
|
||||
{
|
||||
// Non-publishable keys: the *_settings_id keys are also in PresetCollection::skipped_in_dirty
|
||||
// (Preset.cpp) / stripped from configs (profile_print_params_same); publishing them would
|
||||
// rewrite the user's preset inheritance/structure.
|
||||
static const std::set<std::string> structural_keys = {
|
||||
"printer_settings_id", "filament_settings_id", "print_settings_id",
|
||||
"sla_print_settings_id", "sla_material_settings_id",
|
||||
"compatible_printers", "compatible_prints",
|
||||
"compatible_printers_condition", "compatible_prints_condition",
|
||||
"default_filament_profile", "default_print_profile",
|
||||
"default_sla_print_profile", "default_sla_material_profile",
|
||||
"extruder_count", "bed_shape",
|
||||
"inherits", "inherits_group",
|
||||
"printer_technology", "printer_model", "printer_variant",
|
||||
"physical_printer_settings_id", "filament_ids",
|
||||
"different_settings_to_system"
|
||||
};
|
||||
return structural_keys;
|
||||
}
|
||||
|
||||
const std::set<std::string>& publish_mixed_keys()
|
||||
{
|
||||
// Must match PresetBundle's s_project_options mixed-color group (PresetBundle.cpp): these
|
||||
// are project-level parallel per-slot arrays, not filament-preset options, so the import
|
||||
// material pass applies them into project_config instead of a filament preset config.
|
||||
static const std::set<std::string> mixed_keys = {
|
||||
"filament_is_mixed",
|
||||
"filament_mixed_components",
|
||||
"filament_mixed_sublayer_ratios",
|
||||
"filament_mixed_gradient",
|
||||
"filament_mixed_gradient_range",
|
||||
"filament_mixed_gradient_curve",
|
||||
"filament_mixed_gradient_per_part"
|
||||
};
|
||||
return mixed_keys;
|
||||
}
|
||||
|
||||
// The printer tab's "Retraction" optgroup (TabPrinter::build_fff, Tab.cpp), in tab order.
|
||||
// KEEP IN SYNC with that optgroup: the published-3MF printer allowlist is built from these
|
||||
// lists, so any key shown there must be publishable here (and vice versa).
|
||||
const std::vector<PublishablePrinterOption>& publishable_printer_retraction_options()
|
||||
{
|
||||
static const std::vector<PublishablePrinterOption> options = {
|
||||
{ "retraction_length", "printer_extruder_retraction#length" },
|
||||
{ "retract_restart_extra", "printer_extruder_retraction#extra-length-on-restart" },
|
||||
{ "retraction_speed", "printer_extruder_retraction#retraction-speed" },
|
||||
{ "deretraction_speed", "printer_extruder_retraction#deretraction-speed" },
|
||||
{ "retraction_minimum_travel", "printer_extruder_retraction#travel-distance-threshold" },
|
||||
{ "retract_when_changing_layer", "printer_extruder_retraction#retract-on-layer-change" },
|
||||
{ "wipe", "printer_extruder_retraction#wipe-while-retracting" },
|
||||
{ "wipe_distance", "printer_extruder_retraction#wipe-distance" },
|
||||
{ "retract_before_wipe", "printer_extruder_retraction#retract-amount-before-wipe" },
|
||||
{ "retract_after_wipe", "printer_extruder_retraction#retract-amount-after-wipe" },
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
// The printer tab's "Z-Hop" optgroup (TabPrinter::build_fff, Tab.cpp), in tab order. KEEP IN
|
||||
// SYNC with that optgroup, same as publishable_printer_retraction_options().
|
||||
const std::vector<PublishablePrinterOption>& publishable_printer_z_hop_options()
|
||||
{
|
||||
static const std::vector<PublishablePrinterOption> options = {
|
||||
{ "retract_lift_enforce", "printer_extruder_z_hop#on-surfaces" },
|
||||
{ "z_hop_types", "printer_extruder_z_hop#z-hop-type" },
|
||||
{ "z_hop", "printer_extruder_z_hop#z-hop-height" },
|
||||
{ "travel_slope", "printer_extruder_z_hop#traveling-angle" },
|
||||
{ "retract_lift_above", "printer_extruder_z_hop#only-lift-z-above" },
|
||||
{ "retract_lift_below", "printer_extruder_z_hop#only-lift-z-below" },
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
const std::set<std::string>& publishable_printer_keys()
|
||||
{
|
||||
// Union of the two optgroups; "Retraction when switching material" keys are excluded
|
||||
// (toolchange retraction is device/profile territory, not a publishable behavior tweak).
|
||||
static const std::set<std::string> printer_keys = [] {
|
||||
std::set<std::string> keys;
|
||||
for (const PublishablePrinterOption &opt : publishable_printer_retraction_options())
|
||||
keys.insert(opt.key);
|
||||
for (const PublishablePrinterOption &opt : publishable_printer_z_hop_options())
|
||||
keys.insert(opt.key);
|
||||
return keys;
|
||||
}();
|
||||
return printer_keys;
|
||||
}
|
||||
|
||||
std::vector<std::string> collect_dirty_settings_keys(const PresetBundle& bundle)
|
||||
{
|
||||
std::set<std::string> keys;
|
||||
|
||||
// Union the dirty keys of each collection's edited preset (filaments may span multiple
|
||||
// slots); feeds only the Publish dialog's pre-check.
|
||||
for (const std::string& key : bundle.prints.current_dirty_options(true))
|
||||
keys.insert(key);
|
||||
for (const std::string& key : bundle.printers.current_dirty_options(true))
|
||||
keys.insert(key);
|
||||
for (const std::string& key : bundle.filaments.current_dirty_options(true))
|
||||
keys.insert(key);
|
||||
|
||||
return std::vector<std::string>(keys.begin(), keys.end());
|
||||
}
|
||||
|
||||
DynamicPrintConfig filter_published_config(
|
||||
const DynamicPrintConfig &full_config,
|
||||
const std::vector<std::string> &published_keys,
|
||||
const std::vector<PublishedMaterialEntry> &material_keys)
|
||||
{
|
||||
DynamicPrintConfig filtered;
|
||||
|
||||
std::set<std::string> base_keys_to_include;
|
||||
// Never masked (whole-vector serialization): identity, plate geometry, process keys and
|
||||
// printer keys without a "#N" variant.
|
||||
std::set<std::string> mask_exempt_keys;
|
||||
// Material entries: base key -> author slots whose values must survive; other slots are
|
||||
// masked to their defaults so a publish (partial or full) does not leak unrelated slot
|
||||
// data.
|
||||
std::map<std::string, std::set<int>> slot_mask_map;
|
||||
|
||||
// 1. Mandatory material identity & slot count keys for 3MF validation/normalization
|
||||
// (filament_ids: exported for validation, denylisted on apply - see publish_structural_keys).
|
||||
static const std::vector<std::string> s_material_identity_keys = {
|
||||
"filament_colour",
|
||||
"filament_type",
|
||||
"filament_vendor",
|
||||
"filament_ids",
|
||||
"filament_diameter",
|
||||
"filament_self_index",
|
||||
"filament_extruder_variant"
|
||||
};
|
||||
for (const std::string &key : s_material_identity_keys) {
|
||||
base_keys_to_include.insert(key);
|
||||
mask_exempt_keys.insert(key);
|
||||
}
|
||||
|
||||
// 2. Published plate / bed geometry keys (wipe tower positioning)
|
||||
static const std::vector<std::string> s_plate_geometry_keys = {
|
||||
"wipe_tower_x",
|
||||
"wipe_tower_y",
|
||||
"wipe_tower_rotation_angle"
|
||||
};
|
||||
for (const std::string &key : s_plate_geometry_keys) {
|
||||
base_keys_to_include.insert(key);
|
||||
mask_exempt_keys.insert(key);
|
||||
}
|
||||
|
||||
// 3. Process and printer published keys. Printer per-extruder keys carry a "#N" variant
|
||||
// (e.g. retraction_length#2): mask the base to the author's extruder index so a partial
|
||||
// publish does not serialize every extruder's value (same slot-masking as the material side).
|
||||
const std::set<std::string> &printer_keys = publishable_printer_keys();
|
||||
for (const std::string &key : published_keys) {
|
||||
const std::string base_key = publish_base_key(key);
|
||||
if (base_key.empty())
|
||||
continue;
|
||||
base_keys_to_include.insert(base_key);
|
||||
if (printer_keys.count(base_key) != 0) {
|
||||
const int variant_idx = publish_variant_index(key, base_key);
|
||||
if (variant_idx >= 0)
|
||||
slot_mask_map[base_key].insert(variant_idx);
|
||||
else
|
||||
mask_exempt_keys.insert(base_key); // bare printer key or malformed variant: whole vector
|
||||
} else {
|
||||
mask_exempt_keys.insert(base_key); // process key: whole vector
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Material-specific published keys. Both partial (entry.keys) and full-publish
|
||||
// (entry.full_keys) entries mask to the author's slot on export (see the copy loop below);
|
||||
// slot-less entries (hand-crafted files) stay unmasked (whole vector).
|
||||
for (const PublishedMaterialEntry &entry : material_keys) {
|
||||
for (const std::string &key : entry.keys) {
|
||||
const std::string base_key = publish_base_key(key);
|
||||
if (base_key.empty())
|
||||
continue;
|
||||
base_keys_to_include.insert(base_key);
|
||||
if (entry.slot >= 0)
|
||||
slot_mask_map[base_key].insert(entry.slot);
|
||||
}
|
||||
for (const std::string &key : entry.full_keys) {
|
||||
const std::string base_key = publish_base_key(key);
|
||||
if (base_key.empty())
|
||||
continue;
|
||||
base_keys_to_include.insert(base_key);
|
||||
if (entry.slot >= 0)
|
||||
slot_mask_map[base_key].insert(entry.slot);
|
||||
}
|
||||
}
|
||||
|
||||
// Masking restores every non-published slot of a vector option with the option default, so
|
||||
// a partial publish does not leak unrelated slot data. can_mask_slots reports whether a key
|
||||
// is maskable at all (vector option plus a registered default of the same type); an
|
||||
// unmaskable key is dropped from the payload entirely instead of shipping the author's
|
||||
// whole vector.
|
||||
auto can_mask_slots = [](const ConfigOption &opt, const ConfigOptionDef *def) -> bool {
|
||||
if (def == nullptr || !def->default_value || def->default_value->type() != opt.type())
|
||||
return false;
|
||||
const auto *vec = dynamic_cast<const ConfigOptionVectorBase *>(&opt);
|
||||
const auto *default_vec = dynamic_cast<const ConfigOptionVectorBase *>(def->default_value.get());
|
||||
return vec != nullptr && vec->size() > 0 && default_vec != nullptr && !default_vec->empty();
|
||||
};
|
||||
auto mask_slots = [](ConfigOption &opt, const ConfigOptionDef *def, const std::set<int> &keep_slots) {
|
||||
auto *vec = dynamic_cast<ConfigOptionVectorBase*>(&opt);
|
||||
for (size_t idx = 0; idx < vec->size(); ++idx)
|
||||
if (keep_slots.count(static_cast<int>(idx)) == 0)
|
||||
vec->set_at(def->default_value.get(), idx, 0);
|
||||
};
|
||||
|
||||
// Copy the selected options from full_config into the filtered config.
|
||||
for (const std::string &key : base_keys_to_include) {
|
||||
const ConfigOption *opt = full_config.option(key);
|
||||
if (opt == nullptr)
|
||||
continue;
|
||||
const auto mask_it = slot_mask_map.find(key);
|
||||
const bool needs_masking = mask_exempt_keys.count(key) == 0 && mask_it != slot_mask_map.end() && !mask_it->second.empty();
|
||||
if (needs_masking && !can_mask_slots(*opt, print_config_def.get(key))) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "publish: dropping unmaskable key \"" << key
|
||||
<< "\" from the published payload (no usable option default)";
|
||||
continue;
|
||||
}
|
||||
ConfigOption *cloned = opt->clone();
|
||||
if (needs_masking)
|
||||
mask_slots(*cloned, print_config_def.get(key), mask_it->second);
|
||||
filtered.set_key_value(key, cloned);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,99 @@
|
||||
#pragma once
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
class PresetBundle;
|
||||
|
||||
// Strip a trailing "#N" variant suffix ("retraction_length#2" -> "retraction_length").
|
||||
std::string publish_base_key(const std::string &key);
|
||||
|
||||
// Structural keys that are never applied onto the receiver's presets when loading a published
|
||||
// 3MF (single source of truth for the denylist); applying them would rewrite the user's preset
|
||||
// inheritance/structure. filament_ids is still exported via the identity list (3MF validation
|
||||
// needs it) - exported, never applied.
|
||||
const std::set<std::string>& publish_structural_keys();
|
||||
|
||||
// The mixed-color filament project keys (parallel per-slot arrays, see PresetBundle's
|
||||
// s_project_options). Import applies them into project_config, not a filament preset.
|
||||
const std::set<std::string>& publish_mixed_keys();
|
||||
|
||||
// One row of the printer tab's "Retraction" / "Z-Hop" optgroups (config key + tab icon id).
|
||||
struct PublishablePrinterOption {
|
||||
const char *key; // config key, e.g. "retraction_length"
|
||||
const char *icon; // tab icon id, e.g. "printer_extruder_retraction#length"
|
||||
};
|
||||
|
||||
// The printer tab's "Retraction" / "Z-Hop" optgroup options, in tab order.
|
||||
const std::vector<PublishablePrinterOption>& publishable_printer_retraction_options();
|
||||
const std::vector<PublishablePrinterOption>& publishable_printer_z_hop_options();
|
||||
|
||||
// Union of the two optgroup option lists; printer keys apply on import only if their base
|
||||
// key is in this allowlist.
|
||||
const std::set<std::string>& publishable_printer_keys();
|
||||
|
||||
// Union of setting keys differing from the base/system preset across the current print,
|
||||
// printer and filament presets (feeds the Publish dialog's pre-check).
|
||||
std::vector<std::string> collect_dirty_settings_keys(const PresetBundle& bundle);
|
||||
|
||||
// Per-slot published material keys, applied positionally (author slot N -> receiver slot N).
|
||||
// The identity fields drive the created copy's naming and grouping on Full entries, the
|
||||
// notification labels, and the partial type gate (publish_type) is the author's explicit
|
||||
// opt-in for requiring a material type.
|
||||
struct PublishedMaterialEntry {
|
||||
std::string filament_type; // material family, e.g. "PLA" (may be empty)
|
||||
std::string filament_vendor; // e.g. "Generic", "Bambu" (may be empty)
|
||||
std::string filament_id; // stable material id, e.g. "GFL99" (may be empty)
|
||||
// Unique preset id of the author's slot preset (e.g. Orca Filament Library "setting_id").
|
||||
// Not matched against the receiver's library; carried so identical Full entries within one
|
||||
// load share one created instance (within-load dedup key).
|
||||
std::string setting_id;
|
||||
// Canonical name of the author's slot preset (e.g. "Generic PLA @System"). On Full import
|
||||
// it names the created copy after its "@variant" tail is stripped; never matched against
|
||||
// the receiver's library.
|
||||
std::string preset_name;
|
||||
// 0-based author filament slot; -1 (hand-crafted files) is skipped.
|
||||
int slot{-1};
|
||||
std::vector<std::string> keys;
|
||||
// "Full Publish": the whole filament preset (full_keys) is published. On the receiver Full
|
||||
// Publish always creates a standalone parentless copy (libslic3r's "Detach from parent"),
|
||||
// universally compatible and project-embedded only - never written to the user's library.
|
||||
// Identical Full entries within one load share one created instance (within-load dedup).
|
||||
bool full{false};
|
||||
// All non-structural filament keys of the author's slot preset; values travel in the file
|
||||
// config, masked to the author's slot index.
|
||||
std::vector<std::string> full_keys;
|
||||
// Vendor-agnostic (MaterialType) filament type the author requires for this slot; on a
|
||||
// partial entry's mismatch the slot is replaced with a same-type filament. Full entries
|
||||
// consult no gate.
|
||||
bool publish_type{false};
|
||||
std::string publish_type_value;
|
||||
// Required filament colour, applied on load regardless of the type match.
|
||||
bool publish_color{false};
|
||||
std::string color;
|
||||
// Import-side only, never serialized: the authored slot sits past the receiver's physical
|
||||
// capacity, so the entry is appended as an empty mixed-filament placeholder (virtual tail
|
||||
// slot; the GUI flags it for the user to assign components).
|
||||
bool mixed_placeholder{false};
|
||||
};
|
||||
|
||||
// "PLA High Speed" -> "PLA" (strip a space modifier); dash types like "PA-CF" are kept intact.
|
||||
std::string normalize_filament_type(const std::string& type);
|
||||
|
||||
class DynamicPrintConfig;
|
||||
// Clear the compatibility lists/conditions on a filament config so it is universally
|
||||
// compatible once detached (empty lists + empty conditions = compatible with everything).
|
||||
void make_publish_universal(DynamicPrintConfig &config);
|
||||
|
||||
// Naming base for a detached published-material copy: "Generic PLA @System" -> "Generic PLA"
|
||||
// (truncate/right-trim at the first '@' tail). Empty result means "fall back to identity".
|
||||
std::string publish_material_base_name(const std::string &preset_name);
|
||||
|
||||
// Minimal DynamicPrintConfig for a published 3MF export: only the selected published keys,
|
||||
// material keys, identity fields and plate geometry keys.
|
||||
DynamicPrintConfig filter_published_config(
|
||||
const DynamicPrintConfig &full_config,
|
||||
const std::vector<std::string> &published_keys,
|
||||
const std::vector<PublishedMaterialEntry> &material_keys);
|
||||
}
|
||||
@@ -1007,7 +1007,9 @@ bool SLAPrintObject::invalidate_step(SLAPrintObjectStep step)
|
||||
|
||||
bool SLAPrintObject::invalidate_all_steps()
|
||||
{
|
||||
return Inherited::invalidate_all_steps() | m_print->invalidate_all_steps();
|
||||
const bool inherited_invalidated = Inherited::invalidate_all_steps();
|
||||
const bool print_invalidated = m_print->invalidate_all_steps();
|
||||
return inherited_invalidated || print_invalidated;
|
||||
}
|
||||
|
||||
double SLAPrintObject::get_elevation() const {
|
||||
|
||||
@@ -333,8 +333,7 @@ PrintObjectSupportMaterial::PrintObjectSupportMaterial(const PrintObject *object
|
||||
m_print_config (&object->print()->config()),
|
||||
m_object_config (&object->config()),
|
||||
m_slicing_params (slicing_params),
|
||||
m_support_params (*object),
|
||||
m_object (object)
|
||||
m_support_params (*object)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,6 @@ private:
|
||||
*/
|
||||
|
||||
// Following objects are not owned by SupportMaterial class.
|
||||
const PrintObject *m_object;
|
||||
const PrintConfig *m_print_config;
|
||||
const PrintObjectConfig *m_object_config;
|
||||
// Pre-calculated parameters shared between the object slicer and the support generator,
|
||||
|
||||
@@ -2846,7 +2846,9 @@ void TreeSupport::drop_nodes()
|
||||
const MinimumSpanningTree& mst = spanning_trees[group_index];
|
||||
//In the first pass, merge all nodes that are close together.
|
||||
std::vector<std::pair<const Point, SupportNode*>> nodes_vec(nodes_this_part.begin(), nodes_this_part.end());
|
||||
tbb::parallel_for_each(nodes_vec.begin(), nodes_vec.end(), [&](const std::pair<const Point, SupportNode*>& entry) {
|
||||
// Sequential: nodes merge into and invalidate each other in place, so parallel execution
|
||||
// makes the merge order (and thus the result) depend on thread scheduling.
|
||||
std::for_each(nodes_vec.begin(), nodes_vec.end(), [&](const std::pair<const Point, SupportNode*>& entry) {
|
||||
SupportNode* p_node = entry.second;
|
||||
SupportNode& node = *p_node;
|
||||
if (!p_node->valid)
|
||||
@@ -2934,7 +2936,32 @@ void TreeSupport::drop_nodes()
|
||||
);
|
||||
|
||||
//In the second pass, move all middle nodes.
|
||||
tbb::parallel_for_each(nodes_vec.begin(), nodes_vec.end(), [&](const std::pair<const Point, SupportNode*>& entry) {
|
||||
// Still parallel: this pass only reads other nodes. Side effects (invalidation, new
|
||||
// nodes, contact_nodes/unsupported_branch_leaves updates) are recorded per node and
|
||||
// applied afterwards in node order. Node creation must be deferred too, since
|
||||
// SupportNode's constructor writes `parent->child = this` on other nodes.
|
||||
struct PendingNode {
|
||||
Point position;
|
||||
int distance_to_top = 0;
|
||||
int support_roof_layers_below = 0;
|
||||
bool to_buildplate = false;
|
||||
SupportNode *parent = nullptr;
|
||||
bool zero_max_move = false;
|
||||
bool has_overhang = false;
|
||||
ExPolygon overhang;
|
||||
bool clamp_radius = false;
|
||||
coordf_t parent_radius = 0;
|
||||
double dist_to_outer = 0;
|
||||
};
|
||||
struct PassTwoResult {
|
||||
bool invalidate = false;
|
||||
bool unsupported_leaf = false;
|
||||
std::vector<PendingNode> pending;
|
||||
};
|
||||
std::vector<PassTwoResult> pass2_results(nodes_vec.size());
|
||||
auto pass2_body = [&](size_t node_idx) {
|
||||
const std::pair<const Point, SupportNode*>& entry = nodes_vec[node_idx];
|
||||
PassTwoResult& pass2_out = pass2_results[node_idx];
|
||||
|
||||
SupportNode* p_node = entry.second;
|
||||
const SupportNode& node = *p_node;
|
||||
@@ -2949,14 +2976,16 @@ void TreeSupport::drop_nodes()
|
||||
ExPolygons overhangs_next = diff_clipped({ node.overhang }, get_collision(0, obj_layer_nr_next));
|
||||
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),
|
||||
to_buildplate, p_node, print_z_next, height_next);
|
||||
next_node->max_move_dist = 0;
|
||||
next_node->overhang = std::move(overhang);
|
||||
m_ts_data->m_mutex.lock();
|
||||
contact_nodes[layer_nr_next].emplace_back(next_node);
|
||||
m_ts_data->m_mutex.unlock();
|
||||
PendingNode pending;
|
||||
pending.position = next_pt;
|
||||
pending.distance_to_top = p_node->distance_to_top + 1;
|
||||
pending.support_roof_layers_below = p_node->support_roof_layers_below - (p_node->distance_to_top >= 0 ? 1 : 0);
|
||||
pending.to_buildplate = to_buildplate;
|
||||
pending.parent = p_node;
|
||||
pending.zero_max_move = true;
|
||||
pending.has_overhang = true;
|
||||
pending.overhang = std::move(overhang);
|
||||
pass2_out.pending.emplace_back(std::move(pending));
|
||||
|
||||
}
|
||||
return;
|
||||
@@ -2973,17 +3002,17 @@ void TreeSupport::drop_nodes()
|
||||
{
|
||||
if (support_on_buildplate_only)
|
||||
{
|
||||
unsupported_branch_leaves.push_front({ layer_nr, p_node });
|
||||
pass2_out.unsupported_leaf = true;
|
||||
}
|
||||
else {
|
||||
p_node->valid = false;
|
||||
pass2_out.invalidate = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// if the link between parent and current is cut by contours, mark current as bottom contact node
|
||||
if (p_node->parent && intersection_ln({p_node->position, p_node->parent->position}, layer_contours).empty()==false)
|
||||
{
|
||||
p_node->valid = false;
|
||||
pass2_out.invalidate = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -3096,20 +3125,47 @@ 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),
|
||||
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);
|
||||
to_outside = projection_onto(next_collision, next_layer_vertex);
|
||||
direction_to_outer = to_outside - node.position;
|
||||
double dist_to_outer = unscale_(direction_to_outer.cast<double>().norm());
|
||||
next_node->radius = std::max(node.radius, std::min(next_node->radius, dist_to_outer));
|
||||
get_max_move_dist(next_node);
|
||||
m_ts_data->m_mutex.lock();
|
||||
contact_nodes[layer_nr_next].push_back(next_node);
|
||||
m_ts_data->m_mutex.unlock();
|
||||
PendingNode pending;
|
||||
pending.position = next_layer_vertex;
|
||||
pending.distance_to_top = node.distance_to_top + 1;
|
||||
pending.support_roof_layers_below = node.support_roof_layers_below - (node.distance_to_top >= 0 ? 1 : 0);
|
||||
pending.to_buildplate = to_buildplate;
|
||||
pending.parent = p_node;
|
||||
pending.clamp_radius = true;
|
||||
pending.parent_radius = node.radius;
|
||||
pending.dist_to_outer = dist_to_outer;
|
||||
pass2_out.pending.emplace_back(std::move(pending));
|
||||
};
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, nodes_vec.size()),
|
||||
[&pass2_body](const tbb::blocked_range<size_t>& node_range) {
|
||||
for (size_t node_idx = node_range.begin(); node_idx < node_range.end(); ++ node_idx)
|
||||
pass2_body(node_idx);
|
||||
});
|
||||
// Apply the recorded side effects in node order.
|
||||
for (size_t node_idx = 0; node_idx < nodes_vec.size(); ++ node_idx) {
|
||||
PassTwoResult& pass2_out = pass2_results[node_idx];
|
||||
for (PendingNode& pending : pass2_out.pending) {
|
||||
SupportNode* next_node = m_ts_data->create_node(pending.position, pending.distance_to_top, obj_layer_nr_next,
|
||||
pending.support_roof_layers_below, pending.to_buildplate, pending.parent, print_z_next, height_next);
|
||||
if (pending.zero_max_move)
|
||||
next_node->max_move_dist = 0;
|
||||
if (pending.has_overhang)
|
||||
next_node->overhang = std::move(pending.overhang);
|
||||
if (pending.clamp_radius) {
|
||||
next_node->radius = std::max(pending.parent_radius, std::min(next_node->radius, pending.dist_to_outer));
|
||||
get_max_move_dist(next_node);
|
||||
}
|
||||
contact_nodes[layer_nr_next].push_back(next_node);
|
||||
}
|
||||
if (pass2_out.unsupported_leaf)
|
||||
unsupported_branch_leaves.push_front({ layer_nr, nodes_vec[node_idx].second });
|
||||
if (pass2_out.invalidate)
|
||||
nodes_vec[node_idx].second->valid = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_TREE_DEBUG_TO_SVG
|
||||
|
||||
@@ -432,7 +432,6 @@ private:
|
||||
size_t m_highest_overhang_layer = 0;
|
||||
std::vector<std::vector<MinimumSpanningTree>> m_spanning_trees;
|
||||
std::vector< std::unordered_map<Line, bool, LineHash>> m_mst_line_x_layer_contour_caches;
|
||||
float DO_NOT_MOVER_UNDER_MM = 0.0;
|
||||
coordf_t base_radius = 0.0;
|
||||
const coordf_t MAX_BRANCH_RADIUS = 10.0;
|
||||
const coordf_t MIN_BRANCH_RADIUS = 0.4;
|
||||
|
||||
@@ -2382,13 +2382,10 @@ static void merge_influence_areas(
|
||||
size_t num_buckets_initial;
|
||||
{
|
||||
// How many buckets per first merge iteration?
|
||||
const size_t num_threads = tbb::this_task_arena::max_concurrency();
|
||||
// 4 buckets per thread if possible,
|
||||
const size_t num_buckets_min = (input_size + 2) / 4;
|
||||
// 2 buckets per thread otherwise.
|
||||
const size_t num_buckets_max = input_size / 2;
|
||||
num_buckets_initial = num_buckets_min >= num_threads ? num_buckets_min : num_buckets_max;
|
||||
const size_t bucket_size = num_buckets_min >= num_threads ? 4 : 2;
|
||||
// Fixed at 4: merging is not associative, so sizing buckets off max_concurrency() made
|
||||
// results depend on the core count of the slicing machine.
|
||||
const size_t bucket_size = 4;
|
||||
num_buckets_initial = (input_size + 2) / 4;
|
||||
// Fill in the buckets.
|
||||
SupportElementMerging *it = influence_areas.data();
|
||||
// Reserve one more bucket to keep a single influence area which will not be merged in the first iteration.
|
||||
|
||||
@@ -30,6 +30,11 @@ static HMODULE s_hKernel32 = nullptr;
|
||||
static SetThreadDescriptionType s_fnSetThreadDescription = nullptr;
|
||||
static GetThreadDescriptionType s_fnGetThreadDescription = nullptr;
|
||||
|
||||
// Convert the FARPROC from GetProcAddress to Fn through a generic function pointer.
|
||||
template<typename Fn> static Fn load_proc(HMODULE module, const char* name) {
|
||||
return reinterpret_cast<Fn>(reinterpret_cast<void(*)()>(::GetProcAddress(module, name)));
|
||||
}
|
||||
|
||||
static bool WindowsGetSetThreadNameAPIInitialize()
|
||||
{
|
||||
if (! s_SetGetThreadDescriptionInitialized) {
|
||||
@@ -37,8 +42,8 @@ static bool WindowsGetSetThreadNameAPIInitialize()
|
||||
// to initialize
|
||||
s_hKernel32 = LoadLibraryW(L"Kernel32.dll");
|
||||
if (s_hKernel32) {
|
||||
s_fnSetThreadDescription = (SetThreadDescriptionType)::GetProcAddress(s_hKernel32, "SetThreadDescription");
|
||||
s_fnGetThreadDescription = (GetThreadDescriptionType)::GetProcAddress(s_hKernel32, "GetThreadDescription");
|
||||
s_fnSetThreadDescription = load_proc<SetThreadDescriptionType>(s_hKernel32, "SetThreadDescription");
|
||||
s_fnGetThreadDescription = load_proc<GetThreadDescriptionType>(s_hKernel32, "GetThreadDescription");
|
||||
}
|
||||
s_SetGetThreadDescriptionInitialized = true;
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
#define CLI_FILAMENT_CAN_NOT_MAP -66
|
||||
#define CLI_ONLY_ONE_TPU_SUPPORTED -67
|
||||
#define CLI_FILAMENTS_NOT_SUPPORTED_BY_EXTRUDER -68
|
||||
#define CLI_MIXED_FILAMENT_INVALID -69
|
||||
|
||||
#define CLI_SLICING_ERROR -100
|
||||
#define CLI_GCODE_PATH_CONFLICTS -101
|
||||
@@ -255,6 +256,10 @@ extern bool is_gallery_file(const std::string& path, char const* type);
|
||||
extern bool is_shapes_dir(const std::string& dir);
|
||||
//BBS: add json support
|
||||
extern bool is_json_file(const std::string& path);
|
||||
// True if rel_path is relative, has no ".." component and, joined to root, still resolves inside it.
|
||||
// Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS
|
||||
// is rejected on all of them.
|
||||
extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root);
|
||||
|
||||
// Orca: custom protocal support utils
|
||||
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
||||
@@ -309,6 +314,9 @@ extern unsigned get_current_pid();
|
||||
std::string per_user_temp_id();
|
||||
// Per-user temp root under `base`; an empty `user_id` returns `base` unchanged.
|
||||
std::string per_user_temp_dir(const std::string &base, const std::string &user_id);
|
||||
// Completes a relative command line input path against the current working directory. Absolute
|
||||
// paths and custom open protocol URLs are returned unchanged.
|
||||
std::string resolve_cli_input_path(const std::string &path);
|
||||
// BBS: backup & restore
|
||||
std::string get_process_name(int pid);
|
||||
|
||||
|
||||
@@ -93,6 +93,9 @@ static constexpr double INSET_OVERLAP_TOLERANCE = 0.4;
|
||||
static constexpr double EXTERNAL_INFILL_MARGIN = 3;
|
||||
static constexpr double BRIDGE_INFILL_MARGIN = 1;
|
||||
static constexpr double WIPE_TOWER_MARGIN = 1.;
|
||||
// Margin for system placement of the wipe tower (defaults, re-placement, CLI). Positions
|
||||
// within WIPE_TOWER_MARGIN stay valid: a user drag down to that limit is respected.
|
||||
static constexpr double WIPE_TOWER_AUTO_MARGIN = 15.;
|
||||
//FIXME Better to use an inline function with an explicit return type.
|
||||
//inline coord_t scale_(coordf_t v) { return coord_t(floor(v / SCALING_FACTOR + 0.5f)); }
|
||||
#define scale_(val) ((val) / SCALING_FACTOR)
|
||||
|
||||
+38
-1
@@ -961,7 +961,7 @@ CopyFileResult copy_file(const std::string &from, const std::string &to, std::st
|
||||
BOOL result = CopyFileW(src_wstr, dst_wstr, FALSE);
|
||||
if (!result) {
|
||||
DWORD errCode = GetLastError();
|
||||
error_message = "Error: " + errCode;
|
||||
error_message = "Error: " + std::to_string(errCode);
|
||||
ret = FAIL_COPY_FILE;
|
||||
goto __finished;
|
||||
}
|
||||
@@ -1088,6 +1088,30 @@ bool is_json_file(const std::string& path)
|
||||
return boost::iends_with(path, ".json");
|
||||
}
|
||||
|
||||
bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root)
|
||||
{
|
||||
auto is_separator = [](char c) { return c == '/' || c == '\\'; };
|
||||
if (rel_path.empty() || is_separator(rel_path.front()) || (rel_path.size() > 1 && rel_path[1] == ':'))
|
||||
return false;
|
||||
for (size_t start = 0; start <= rel_path.size();) {
|
||||
size_t end = start;
|
||||
while (end < rel_path.size() && !is_separator(rel_path[end]))
|
||||
++end;
|
||||
if (rel_path.compare(start, end - start, "..") == 0)
|
||||
return false;
|
||||
start = end + 1;
|
||||
}
|
||||
// Resolve against the canonical root so a symlink inside it cannot lead back out.
|
||||
try {
|
||||
const std::string root_str = boost::filesystem::weakly_canonical(root).string();
|
||||
const std::string full_str = boost::filesystem::weakly_canonical(root / rel_path).string();
|
||||
return full_str.compare(0, root_str.size(), root_str) == 0 &&
|
||||
(full_str.size() == root_str.size() || full_str[root_str.size()] == boost::filesystem::path::preferred_separator);
|
||||
} catch (const boost::filesystem::filesystem_error &) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_img_file(const std::string &path)
|
||||
{
|
||||
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
||||
@@ -1315,6 +1339,19 @@ std::string per_user_temp_dir(const std::string &base, const std::string &user_i
|
||||
return base + "/orcaslicer_" + user_id;
|
||||
}
|
||||
|
||||
std::string resolve_cli_input_path(const std::string &path)
|
||||
{
|
||||
const boost::filesystem::path input(path);
|
||||
if (path.empty() || is_supported_open_protocol(path) || input.is_absolute())
|
||||
return path;
|
||||
|
||||
boost::system::error_code ec;
|
||||
const boost::filesystem::path resolved = boost::filesystem::system_complete(input, ec);
|
||||
if (ec)
|
||||
return path;
|
||||
return resolved.lexically_normal().make_preferred().string();
|
||||
}
|
||||
|
||||
// BBS: backup & restore
|
||||
std::string get_process_name(int pid)
|
||||
{
|
||||
|
||||
@@ -97,6 +97,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/CloneDialog.hpp
|
||||
GUI/ConfigManipulation.cpp
|
||||
GUI/ConfigManipulation.hpp
|
||||
GUI/ConfigValueFormatter.cpp
|
||||
GUI/ConfigValueFormatter.hpp
|
||||
GUI/ConfigWizard.cpp
|
||||
GUI/ConfigWizard.hpp
|
||||
GUI/ConfigWizard_private.hpp
|
||||
@@ -451,6 +453,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Project.hpp
|
||||
GUI/PublishDialog.cpp
|
||||
GUI/PublishDialog.hpp
|
||||
GUI/PublishSettingsDialog.cpp
|
||||
GUI/PublishSettingsDialog.hpp
|
||||
GUI/PurgeModeDialog.cpp
|
||||
GUI/PurgeModeDialog.hpp
|
||||
GUI/RammingChart.cpp
|
||||
@@ -676,6 +680,10 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/bambu_networking.hpp
|
||||
Utils/Bonjour.cpp
|
||||
Utils/Bonjour.hpp
|
||||
Utils/MeshInspect.cpp
|
||||
Utils/MeshInspect.hpp
|
||||
Utils/PaintCLI.cpp
|
||||
Utils/PaintCLI.hpp
|
||||
Utils/CalibUtils.cpp
|
||||
Utils/CalibUtils.hpp
|
||||
Utils/ColorSpaceConvert.cpp
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/GCode/WipeTower.hpp"
|
||||
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
|
||||
#include "libslic3r/Tesselate.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
@@ -919,6 +921,21 @@ int GLVolumeCollection::load_wipe_tower_preview(
|
||||
GUI::PartPlateList& ppl = GUI::wxGetApp().plater()->get_partplate_list();
|
||||
std::vector<int> plate_extruders = ppl.get_plate(plate_idx)->get_extruders(true);
|
||||
TriangleMesh wipe_tower_shell = make_cube(width, depth, height);
|
||||
// The brim is part of the printed footprint: draw it and fold it into the shell so the
|
||||
// outside-bed shader and the drag clamp react to the true first-layer extent.
|
||||
const bool show_brim = brim_width > 0.f;
|
||||
const float brim_height = 0.2f; // one first layer, visual only
|
||||
TriangleMesh brim_slab;
|
||||
if (show_brim) {
|
||||
// The brim follows the real first-layer outline: a Type2 cone-wall tower's base bulges
|
||||
// past the body box. The wall type and angle are print settings, the planner a printer one.
|
||||
const DynamicPrintConfig &print_cfg = GUI::wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
||||
const DynamicPrintConfig &printer_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
||||
const Polygon outline = estimate_wipe_tower_first_layer_outline(print_cfg, resolve_wipe_tower_type(printer_cfg), width, depth, height);
|
||||
const Polygons brim_outline = offset(outline, scaled(brim_width));
|
||||
brim_slab = WipeTower::its_make_rib_brim(brim_outline.empty() ? outline : brim_outline.front(), brim_height);
|
||||
wipe_tower_shell.merge(brim_slab);
|
||||
}
|
||||
for (int extruder_id : plate_extruders) {
|
||||
if (extruder_id <= extruder_colors.size())
|
||||
colors.push_back(extruder_colors[extruder_id - 1]);
|
||||
@@ -929,14 +946,19 @@ int GLVolumeCollection::load_wipe_tower_preview(
|
||||
// Orca: make it transparent
|
||||
for(auto& color : colors)
|
||||
color.a(0.66f);
|
||||
const size_t slab_count = colors.size(); // per-filament body slabs; the brim part comes after
|
||||
if (show_brim && !colors.empty())
|
||||
colors.push_back(colors.front());
|
||||
volumes.emplace_back(new GLWipeTowerVolume(colors));
|
||||
GLWipeTowerVolume& v = *dynamic_cast<GLWipeTowerVolume*>(volumes.back());
|
||||
v.model_per_colors.resize(colors.size());
|
||||
for (int i = 0; i < colors.size(); i++) {
|
||||
TriangleMesh color_part = make_cube(width, depth / colors.size(), height);
|
||||
color_part.translate({ 0.f, depth * i / colors.size(), 0. });
|
||||
for (size_t i = 0; i < slab_count; i++) {
|
||||
TriangleMesh color_part = make_cube(width, depth / slab_count, height);
|
||||
color_part.translate({ 0.f, depth * i / slab_count, 0. });
|
||||
v.model_per_colors[i].init_from(color_part);
|
||||
}
|
||||
if (show_brim && !colors.empty())
|
||||
v.model_per_colors[slab_count].init_from(brim_slab);
|
||||
v.model.init_from(wipe_tower_shell);
|
||||
v.mesh_raycaster = std::make_unique<GUI::MeshRaycaster>(std::make_shared<const TriangleMesh>(wipe_tower_shell));
|
||||
v.set_convex_hull(wipe_tower_shell);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "AMSDryControl.hpp"
|
||||
#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
|
||||
#include "GUI_App.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "I18N.hpp"
|
||||
|
||||
#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h"
|
||||
@@ -1196,11 +1197,11 @@ void AMSDryCtrWin::update_normal_description(DevAms* dev_ams)
|
||||
for (const auto& lim : ams_limits) {
|
||||
if (dev_ams->GetAmsType() == lim.type) {
|
||||
if (temp_val > lim.max_temp) {
|
||||
wxString msg = wxString(lim.name) + _L(" maximum drying temperature is ") + wxString::Format(wxT("%d"), lim.max_temp) + wxString::FromUTF8("°C.");
|
||||
wxString msg = wxString::Format(_L("%s maximum drying temperature is %d°C."), wxString(lim.name), lim.max_temp);
|
||||
warning_text += msg + "\n";
|
||||
can_enable_button = false;
|
||||
} else if (temp_val < lim.min_temp) {
|
||||
wxString msg = wxString(lim.name) + _L(" minimum drying temperature is ") + wxString::Format(wxT("%d"), lim.min_temp) + wxString::FromUTF8("°C.");
|
||||
wxString msg = wxString::Format(_L("%s minimum drying temperature is %d°C."), wxString(lim.name), lim.min_temp);
|
||||
warning_text += msg + "\n";
|
||||
can_enable_button = false;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
//Previous defintions
|
||||
class wxGrid;
|
||||
class ProgressBar;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -97,12 +98,6 @@ private:
|
||||
wxSimplebook* m_main_simplebook{nullptr};
|
||||
wxPanel* m_original_page{nullptr};
|
||||
|
||||
wxWindow* m_amswin{nullptr};
|
||||
wxBoxSizer* m_sizer_ams_items{nullptr};
|
||||
wxScrolledWindow* m_panel_prv_left {nullptr};
|
||||
wxScrolledWindow* m_panel_prv_right{nullptr};
|
||||
wxBoxSizer* m_sizer_prv_left{nullptr};
|
||||
wxBoxSizer* m_sizer_prv_right{nullptr};
|
||||
|
||||
// left panel related members
|
||||
ScalableBitmap m_humidity_image;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "ExtrusionCalibration.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "MainFrame.hpp"
|
||||
#include "format.hpp"
|
||||
#include "Widgets/ProgressDialog.hpp"
|
||||
#include <wx/tooltip.h>
|
||||
#include "Widgets/RoundedRectangle.hpp"
|
||||
#include "Widgets/StaticBox.hpp"
|
||||
|
||||
|
||||
@@ -457,7 +457,6 @@ private:
|
||||
ScalableBitmap close_img;
|
||||
|
||||
wxStaticBitmap* curr_humidity_img;
|
||||
wxStaticBitmap* m_img;
|
||||
|
||||
Label* m_staticText;;
|
||||
Label* m_staticText_note;
|
||||
|
||||
@@ -406,7 +406,7 @@ void AmsMapingPopup::update_ams_data_multi_machines()
|
||||
int ams_type = 1;
|
||||
int nozzle_id = 0;
|
||||
|
||||
if (ams_type >= 1 || ams_type <= 3) { // 1:ams 2:ams-lite 3:n3f
|
||||
if (ams_type >= 1 && ams_type <= 3) { // 1:ams 2:ams-lite 3:n3f
|
||||
|
||||
auto sizer_mapping_list = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto ams_mapping_item_container = new MappingContainer(nozzle_id == 0 ? m_right_marea_panel : m_left_marea_panel, "AMS-1", 4);
|
||||
|
||||
@@ -93,7 +93,6 @@ private:
|
||||
CenteredTitle* m_title_ctrl { nullptr };
|
||||
wxString m_titleText;
|
||||
|
||||
wxAuiToolBarItem* m_model_store_item;
|
||||
|
||||
//wxAuiToolBarItem *m_publish_item;
|
||||
wxAuiToolBarItem* m_undo_item;
|
||||
|
||||
@@ -848,7 +848,9 @@ void BackgroundSlicingProcess::finalize_gcode()
|
||||
case CopyFileResult::SUCCESS: break; // no error
|
||||
case CopyFileResult::FAIL_COPY_FILE:
|
||||
throw Slic3r::ExportError(GUI::format(
|
||||
_L("Copying of the temporary G-code to the output G-code failed. Maybe the SD card is write locked?\nError message: %1%"),
|
||||
m_export_path_on_removable_media ?
|
||||
_L("Copying of the temporary G-code to the output G-code failed. Maybe the SD card is write locked?\nError message: %1%") :
|
||||
_L("Copying of the temporary G-code to the output G-code failed.\nError message: %1%"),
|
||||
error_message));
|
||||
break;
|
||||
case CopyFileResult::FAIL_FILES_DIFFERENT:
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
#include <thread>
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI_Utils.hpp"
|
||||
#include <wx/timer.h>
|
||||
|
||||
class Button;
|
||||
class Label;
|
||||
class CheckBox;
|
||||
namespace Slic3r { namespace GUI {
|
||||
class CapsuleButton;
|
||||
|
||||
@@ -65,18 +65,10 @@ private:
|
||||
wxPanel* request_bind_panel;
|
||||
wxPanel* binding_panel;
|
||||
|
||||
wxScrolledWindow* m_sw_bind_failed_info;
|
||||
Label* m_bind_failed_info;
|
||||
Label* m_st_txt_error_code{ nullptr };
|
||||
Label* m_st_txt_error_desc{ nullptr };
|
||||
Label* m_st_txt_extra_info{ nullptr };
|
||||
HyperLink* m_link_network_state{ nullptr };
|
||||
wxString m_result_info;
|
||||
wxString m_result_extra;
|
||||
wxString m_ping_code_wiki;
|
||||
bool m_show_error_info_state = true;
|
||||
|
||||
int m_result_code;
|
||||
std::shared_ptr<BBLStatusBarBind> m_status_bar;
|
||||
|
||||
public:
|
||||
@@ -110,7 +102,6 @@ private:
|
||||
wxBitmap m_bitmap_show_error_close;
|
||||
wxBitmap m_bitmap_show_error_open;
|
||||
wxScrolledWindow* m_sw_bind_failed_info;
|
||||
Label* m_bind_failed_info;
|
||||
Label* m_st_txt_error_code{ nullptr };
|
||||
Label* m_st_txt_error_desc{ nullptr };
|
||||
Label* m_st_txt_extra_info{ nullptr };
|
||||
|
||||
@@ -70,11 +70,7 @@ public:
|
||||
|
||||
private:
|
||||
int m_my_devices_count{ 0 };
|
||||
int m_other_devices_count{ 0 };
|
||||
bool m_dismiss{ false };
|
||||
wxWindow* m_placeholder_panel { nullptr };
|
||||
wxWindow* m_panel_body{ nullptr };
|
||||
wxBoxSizer* m_sizer_body{ nullptr };
|
||||
wxBoxSizer* m_sizer_my_devices{ nullptr };
|
||||
wxScrolledWindow* m_scrolledWindow{ nullptr };
|
||||
wxTimer* m_refresh_timer{ nullptr };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "CalibrationWizard.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "CalibrationWizardPage.hpp"
|
||||
#include "../../libslic3r/calib.hpp"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include <regex>
|
||||
#include "CalibrationWizardPresetPage.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Widgets/Label.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
@@ -360,7 +362,7 @@ void CaliPresetCustomRangePanel::create_panel(wxWindow* parent)
|
||||
int max_decimal_length;
|
||||
if (i <= 1)
|
||||
max_decimal_length = 3;
|
||||
else if (i >= 2)
|
||||
else
|
||||
max_decimal_length = 4;
|
||||
if (decimal_number > max_decimal_length) {
|
||||
int allowed_length = number.length() - decimal_number + max_decimal_length;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "CalibrationWizardSavePage.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Widgets/Label.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
|
||||
@@ -72,8 +72,10 @@ private:
|
||||
SwitchButton* m_switch_recording;
|
||||
wxStaticText* m_text_vcamera;
|
||||
SwitchButton* m_switch_vcamera;
|
||||
#if !BBL_RELEASE_TO_PUBLIC
|
||||
wxStaticText* m_text_liveview_retry;
|
||||
SwitchButton* m_switch_liveview_retry;
|
||||
#endif //BBL_RELEASE_TO_PUBLIC
|
||||
wxStaticText* m_custom_camera_hint;
|
||||
TextInput* m_custom_camera_input;
|
||||
Button* m_custom_camera_input_confirm;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "CapsuleButton.hpp"
|
||||
#include "Widgets/StateColor.hpp"
|
||||
#include <wx/dcbuffer.h>
|
||||
#include "wx/graphics.h"
|
||||
#include "Widgets/Label.hpp"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ColorDecomposeSupport.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "MixedFilamentDialog.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
@@ -394,8 +395,9 @@ bool confirm_create_decompose_missing_components(wxWindow* parent, const std::ve
|
||||
missing_text += missing[i].display_name;
|
||||
}
|
||||
|
||||
wxString message = _L("The current filament list does not contain ") + missing_text +
|
||||
_L(". A project filament required by the mixed filament will be created automatically after decomposition.");
|
||||
wxString message = wxString::Format(_L("The current filament list does not contain %s. A project filament required by "
|
||||
"the mixed filament will be created automatically after decomposition."),
|
||||
missing_text);
|
||||
|
||||
MessageDialog dlg(parent, message, _L("Tip"), wxOK | wxCANCEL | wxICON_INFORMATION);
|
||||
dlg.show_dsa_button();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "ConfigManipulation.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "DeviceCore/DevConfigUtil.h"
|
||||
#include "format.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
@@ -1040,10 +1041,12 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
|
||||
for (auto el : {"wipe_tower_rotation_angle", "wipe_tower_cone_angle",
|
||||
"wipe_tower_extra_spacing", "wipe_tower_max_purge_speed",
|
||||
"wipe_tower_bridging", "wipe_tower_extra_flow",
|
||||
"wipe_tower_no_sparse_layers"})
|
||||
"wipe_tower_bridging", "wipe_tower_extra_flow"})
|
||||
toggle_line(el, have_prime_tower && supports_wipe_tower_2);
|
||||
|
||||
// Orca: both tower generators skip sparse layers, so this is not a wipe tower 2 exclusive.
|
||||
toggle_line("wipe_tower_no_sparse_layers", have_prime_tower);
|
||||
|
||||
WipeTowerWallType wipe_tower_wall_type = config->opt_enum<WipeTowerWallType>("wipe_tower_wall_type");
|
||||
bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower;
|
||||
toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && wipe_tower_wall_type == WipeTowerWallType::wtwCone);
|
||||
@@ -1054,6 +1057,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
|
||||
toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2);
|
||||
|
||||
bool use_cyclic_ordering = config->opt_enum<ToolChangeOrderingType>("toolchange_ordering") == ToolChangeOrderingType::Cyclic;
|
||||
toggle_line("toolchange_cyclic_order", use_cyclic_ordering);
|
||||
toggle_line("toolchange_cyclic_first_layer", use_cyclic_ordering);
|
||||
|
||||
toggle_line("prime_volume",have_prime_tower && (!purge_in_primetower || !bSEMM));
|
||||
|
||||
for (auto el : {"flush_into_infill", "flush_into_support", "flush_into_objects"})
|
||||
@@ -1103,6 +1110,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
auto is_role_based_wipe_speed = config->opt_bool("role_based_wipe_speed");
|
||||
toggle_field("wipe_speed",!is_role_based_wipe_speed);
|
||||
|
||||
const bool have_wipe_inward = config->opt_bool("wipe_inward");
|
||||
toggle_line("wipe_inward_distance", have_wipe_inward);
|
||||
|
||||
for (auto el : {"accel_to_decel_enable", "accel_to_decel_factor"})
|
||||
toggle_line(el, gcf_is_klipper);
|
||||
if(gcf_is_klipper)
|
||||
@@ -1124,6 +1134,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
bool has_detect_overhang_wall = config->opt_bool("detect_overhang_wall");
|
||||
bool has_overhang_reverse = config->opt_bool("overhang_reverse");
|
||||
bool allow_overhang_reverse = !has_spiral_vase;
|
||||
toggle_line("unsupported_wall_last", has_detect_overhang_wall);
|
||||
toggle_line("overhang_reverse", allow_overhang_reverse);
|
||||
toggle_line("overhang_reverse_internal_only", allow_overhang_reverse && has_overhang_reverse);
|
||||
bool has_overhang_reverse_internal_only = config->opt_bool("overhang_reverse_internal_only");
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
#include "ConfigValueFormatter.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/format.hpp>
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
#include "I18N.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "Field.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
std::string get_pure_opt_key(const std::string& opt_key)
|
||||
{
|
||||
std::string pure_key = opt_key;
|
||||
const int pos = pure_key.find("#");
|
||||
if (pos > 0)
|
||||
boost::erase_tail(pure_key, pure_key.size() - pos);
|
||||
return pure_key;
|
||||
}
|
||||
|
||||
wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill, int idx)
|
||||
{
|
||||
const ConfigOptionDef& def = config.def()->options.at(opt_key);
|
||||
const std::vector<std::string>& names = def.enum_labels;//ConfigOptionEnum<T>::get_enum_names();
|
||||
int val = 0;
|
||||
|
||||
if (idx >= 0)
|
||||
val = dynamic_cast<const ConfigOptionInts*>(config.option(opt_key))->get_at(idx);
|
||||
else
|
||||
val = config.option(opt_key)->getInt();
|
||||
|
||||
// Each infill doesn't use all list of infill declared in PrintConfig.hpp.
|
||||
// So we should "convert" val to the correct one
|
||||
if (is_infill) {
|
||||
for (auto key_val : *def.enum_keys_map)
|
||||
if (int(key_val.second) == val) {
|
||||
auto it = std::find(def.enum_values.begin(), def.enum_values.end(), key_val.first);
|
||||
if (it == def.enum_values.end())
|
||||
return "";
|
||||
return from_u8(_utf8(names[it - def.enum_values.begin()]));
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
return from_u8(_utf8(names[val]));
|
||||
}
|
||||
|
||||
wxString get_full_label(const std::string& opt_key, const DynamicPrintConfig& config)
|
||||
{
|
||||
const std::string pure_key = get_pure_opt_key(opt_key);
|
||||
auto option = config.option(pure_key);
|
||||
|
||||
if (!option || option->is_nil())
|
||||
return _L("N/A");
|
||||
|
||||
const ConfigOptionDef* opt = config.def()->get(pure_key);
|
||||
return opt->full_label.empty() ? opt->label : opt->full_label;
|
||||
}
|
||||
|
||||
wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& config)
|
||||
{
|
||||
int orig_opt_idx = -1;
|
||||
int opt_idx = -1;
|
||||
int pos = opt_key.find("#");
|
||||
std::string temp_str = opt_key;
|
||||
if (pos > 0) {
|
||||
boost::erase_head(temp_str, pos + 1);
|
||||
orig_opt_idx = std::atoi(temp_str.c_str());
|
||||
}
|
||||
opt_idx = orig_opt_idx >= 0 ? orig_opt_idx : 0;
|
||||
const std::string pure_key = get_pure_opt_key(opt_key);
|
||||
auto option = config.option(pure_key);
|
||||
if (!option) {
|
||||
return _L("N/A");
|
||||
}
|
||||
auto opt_vector = dynamic_cast<const ConfigOptionVectorBase *>(option);
|
||||
|
||||
if ((option->is_scalar() && option->is_nil()) ||
|
||||
(option->is_vector() && opt_vector && opt_idx >= 0 && opt_idx < opt_vector->size() && opt_vector->is_nil(opt_idx)))
|
||||
return _L("N/A");
|
||||
|
||||
wxString out;
|
||||
|
||||
const ConfigOptionDef* opt = config.def()->get(pure_key);
|
||||
bool is_nullable = opt->nullable;
|
||||
|
||||
switch (opt->type) {
|
||||
case coInt:
|
||||
return from_u8((boost::format("%1%") % config.opt_int(pure_key)).str());
|
||||
case coInts: {
|
||||
if (is_nullable) {
|
||||
auto values = config.opt<ConfigOptionIntsNullable>(pure_key);
|
||||
if (opt_idx < values->size())
|
||||
return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str());
|
||||
}
|
||||
else {
|
||||
auto values = config.opt<ConfigOptionInts>(pure_key);
|
||||
if (orig_opt_idx >= 0 && orig_opt_idx < values->size()) {
|
||||
return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str());
|
||||
}
|
||||
else {
|
||||
std::string value_str;
|
||||
for (int i = 0; i < values->size(); i++) {
|
||||
value_str += std::to_string(values->get_at(i));
|
||||
if (i != values->size() - 1) {
|
||||
value_str += ",";
|
||||
}
|
||||
}
|
||||
return from_u8(value_str);
|
||||
}
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coBool:
|
||||
return config.opt_bool(pure_key) ? "true" : "false";
|
||||
case coBools: {
|
||||
if (is_nullable) {
|
||||
auto values = config.opt<ConfigOptionBoolsNullable>(pure_key);
|
||||
if (opt_idx < values->size())
|
||||
return values->get_at(opt_idx) ? "true" : "false";
|
||||
}
|
||||
else {
|
||||
auto values = config.opt<ConfigOptionBools>(pure_key);
|
||||
if (opt_idx < values->size())
|
||||
return values->get_at(opt_idx) ? "true" : "false";
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coPercent:
|
||||
return from_u8((boost::format("%1%%%") % int(config.optptr(pure_key)->getFloat())).str());
|
||||
case coPercents: {
|
||||
if (is_nullable) {
|
||||
auto values = config.opt<ConfigOptionPercentsNullable>(pure_key);
|
||||
if (opt_idx < values->size())
|
||||
return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str());
|
||||
}
|
||||
else {
|
||||
auto values = config.opt<ConfigOptionPercents>(pure_key);
|
||||
if (opt_idx < values->size())
|
||||
return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str());
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coFloat:
|
||||
return double_to_string(config.opt_float(pure_key));
|
||||
case coFloats: {
|
||||
if (is_nullable) {
|
||||
auto values = config.opt<ConfigOptionFloatsNullable>(pure_key);
|
||||
if (opt_idx < values->size())
|
||||
return double_to_string(values->get_at(opt_idx));
|
||||
}
|
||||
else {
|
||||
auto values = config.opt<ConfigOptionFloats>(pure_key);
|
||||
if (values && opt_idx < values->size())
|
||||
return double_to_string(values->get_at(opt_idx));
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coString:
|
||||
return from_u8(config.opt_string(pure_key));
|
||||
case coStrings: {
|
||||
const ConfigOptionStrings* strings = config.opt<ConfigOptionStrings>(pure_key);
|
||||
if (strings) {
|
||||
if (pure_key == "compatible_printers" || pure_key == "compatible_prints") {
|
||||
if (strings->empty())
|
||||
return _L("All");
|
||||
for (size_t id = 0; id < strings->size(); id++)
|
||||
out += from_u8(strings->get_at(id)) + "\n";
|
||||
out.RemoveLast(1);
|
||||
return out;
|
||||
}
|
||||
if (!strings->empty() && opt_idx < strings->values.size())
|
||||
return from_u8(strings->get_at(opt_idx));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case coFloatOrPercent: {
|
||||
const ConfigOptionFloatOrPercent* opt = config.opt<ConfigOptionFloatOrPercent>(pure_key);
|
||||
if (opt)
|
||||
out = double_to_string(opt->value) + (opt->percent ? "%" : "");
|
||||
return out;
|
||||
}
|
||||
case coFloatsOrPercents: {
|
||||
const auto* values = static_cast<const ConfigOptionVector<FloatOrPercent>*>(option);
|
||||
// Orca: Preset comparison may request the entire vector instead of an indexed entry.
|
||||
if (orig_opt_idx < 0)
|
||||
return from_u8(option->serialize());
|
||||
if (opt_idx < values->size()) {
|
||||
const FloatOrPercent& value = values->get_at(opt_idx);
|
||||
return double_to_string(value.value) + (value.percent ? "%" : "");
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coEnum: {
|
||||
return get_string_from_enum(pure_key, config,
|
||||
pure_key == "top_surface_pattern" ||
|
||||
pure_key == "bottom_surface_pattern" ||
|
||||
pure_key == "internal_solid_infill_pattern" ||
|
||||
pure_key == "sparse_infill_pattern" ||
|
||||
pure_key == "ironing_pattern" ||
|
||||
pure_key == "support_ironing_pattern" ||
|
||||
pure_key == "support_pattern" ||
|
||||
pure_key == "support_interface_pattern")
|
||||
;
|
||||
}
|
||||
case coEnums: {
|
||||
return get_string_from_enum(pure_key, config,
|
||||
pure_key == "top_surface_pattern" ||
|
||||
pure_key == "bottom_surface_pattern" ||
|
||||
pure_key == "internal_solid_infill_pattern" ||
|
||||
pure_key == "sparse_infill_pattern" ||
|
||||
pure_key == "ironing_pattern" ||
|
||||
pure_key == "support_ironing_pattern" ||
|
||||
pure_key == "support_pattern" ||
|
||||
pure_key == "support_interface_pattern"
|
||||
, opt_idx);
|
||||
}
|
||||
case coPoint: {
|
||||
Vec2d val = config.opt<ConfigOptionPoint>(pure_key)->value;
|
||||
return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str());
|
||||
}
|
||||
case coPoints: {
|
||||
//BBS: add bed_exclude_area
|
||||
if (pure_key == "printable_area" || pure_key == "thumbnails") {
|
||||
ConfigOptionPoints points = *config.option<ConfigOptionPoints>(pure_key);
|
||||
//BuildVolume build_volume = {points.values, 0.};
|
||||
return get_thumbnails_string(points.values);
|
||||
}
|
||||
else if (pure_key == "bed_exclude_area") {
|
||||
return get_thumbnails_string(config.option<ConfigOptionPoints>(pure_key)->values);
|
||||
}
|
||||
else if (pure_key == "head_wrap_detect_zone") {
|
||||
return get_thumbnails_string(config.option<ConfigOptionPoints>(pure_key)->values);
|
||||
}
|
||||
else if (pure_key == "wrapping_exclude_area") {
|
||||
return get_thumbnails_string(config.option<ConfigOptionPoints>(pure_key)->values);
|
||||
}
|
||||
Vec2d val = config.opt<ConfigOptionPoints>(pure_key)->get_at(opt_idx);
|
||||
return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str());
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <wx/string.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class DynamicPrintConfig;
|
||||
|
||||
namespace GUI {
|
||||
|
||||
// Human-readable value of opt_key (may carry a "#<index>" suffix) in config.
|
||||
wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& config);
|
||||
|
||||
// Full label of opt_key; "N/A" when the option is not set.
|
||||
wxString get_full_label(const std::string& opt_key, const DynamicPrintConfig& config);
|
||||
|
||||
// Strip the "#<index>" suffix (if any) from the option key.
|
||||
std::string get_pure_opt_key(const std::string& opt_key);
|
||||
|
||||
// Localized label of the currently selected value of an enum option.
|
||||
wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill = false, int idx = -1);
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -74,7 +74,6 @@ private:
|
||||
std::unordered_set<std::string> m_system_filament_types_set;
|
||||
std::set<std::string> m_visible_printers;
|
||||
CreateType m_create_type;
|
||||
Button * m_button_cancel = nullptr;
|
||||
ComboBox * m_filament_vendor_combobox = nullptr;
|
||||
::CheckBox * m_can_not_find_vendor_checkbox = nullptr;
|
||||
ComboBox * m_filament_type_combobox = nullptr;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "DailyTips.hpp"
|
||||
#include "slic3r/GUI/Widgets/Label.hpp"
|
||||
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
@@ -245,7 +246,6 @@ DailyTipsPanel::DailyTipsPanel(bool can_expand, DailyTipsLayout layout)
|
||||
m_width(0),
|
||||
m_height(0),
|
||||
m_can_expand(can_expand),
|
||||
m_layout(layout),
|
||||
m_uid(DailyTipsPanel::uid++),
|
||||
m_dailytips_renderer(std::make_unique<DailyTipsDataRenderer>(layout))
|
||||
{
|
||||
|
||||
@@ -51,7 +51,6 @@ private:
|
||||
int m_uid;
|
||||
bool m_first_enter{ false };
|
||||
bool m_is_dark{ false };
|
||||
DailyTipsLayout m_layout{ DailyTipsLayout::Vertical };
|
||||
float m_fade_opacity{ 1.0f };
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
|
||||
#include "slic3r/GUI/UserNotification.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
@@ -54,7 +54,7 @@ public:
|
||||
void ParseCalibrationConfig(const json& print_json); //cali
|
||||
|
||||
private:
|
||||
MachineObject* m_obj;
|
||||
[[maybe_unused]] MachineObject* m_obj;
|
||||
|
||||
/*configure vals*/
|
||||
// chamber
|
||||
|
||||
@@ -31,7 +31,7 @@ protected:
|
||||
DevExtensionTool(MachineObject* obj);
|
||||
|
||||
private:
|
||||
MachineObject* m_owner = nullptr;
|
||||
[[maybe_unused]] MachineObject* m_owner = nullptr;
|
||||
|
||||
enum MountState
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ public:
|
||||
void SetAutoRefillEnabled(bool enable) { m_enable_auto_refill = enable; }
|
||||
|
||||
private:
|
||||
DevFilaSystem* m_owner = nullptr;
|
||||
[[maybe_unused]] DevFilaSystem* m_owner = nullptr;
|
||||
|
||||
std::optional<bool> m_enable_detect_on_insert = false;
|
||||
bool m_enable_detect_on_powerup = false;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <set>
|
||||
|
||||
#include "DevFilaBlackList.h"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
#include "DevFilaSystem.h"
|
||||
#include "DevManager.h"
|
||||
#include "DevConfigUtil.h"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "DevFilaSystem.h"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "DevNozzleSystem.h" // DevNozzle / DevNozzleSystem for GetNozzleFlowStringByAmsId
|
||||
|
||||
// TODO: remove this include
|
||||
|
||||
@@ -21,7 +21,7 @@ public:
|
||||
const std::vector<DevHMSItem>& GetHMSItems() const { return m_hms_list; };
|
||||
|
||||
private:
|
||||
MachineObject* m_object = nullptr;
|
||||
[[maybe_unused]] MachineObject* m_object = nullptr;
|
||||
|
||||
// all hms for this machine
|
||||
std::vector<DevHMSItem> m_hms_list;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user