mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 02:41:17 +00:00
Merge branch 'main' of https://github.com/OrcaSlicer/OrcaSlicer_priv into feat/printer-agent-impl
This commit is contained in:
+12
-1
@@ -33,7 +33,8 @@ if (SLIC3R_GUI)
|
||||
set (wxWidgets_CONFIG_OPTIONS "--toolkit=gtk${SLIC3R_GTK}")
|
||||
find_package(wxWidgets 3.3 REQUIRED COMPONENTS base core adv html gl aui net media webview)
|
||||
else ()
|
||||
find_package(wxWidgets 3.3 CONFIG REQUIRED COMPONENTS html adv gl core base webview aui net media)
|
||||
# propgrid is required by wxInspector.
|
||||
find_package(wxWidgets 3.3 CONFIG REQUIRED COMPONENTS html adv gl core base webview aui net media propgrid)
|
||||
endif ()
|
||||
|
||||
if(UNIX)
|
||||
@@ -90,6 +91,16 @@ if (SLIC3R_GUI)
|
||||
# list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc)
|
||||
|
||||
find_package(wxInspector REQUIRED)
|
||||
|
||||
# wxInspector's exported interface names the release wxWidgets import
|
||||
# libraries, which a Debug build cannot link. wx is linked above instead.
|
||||
get_target_property(_wxinspector_interface wxInspector::wxInspector INTERFACE_LINK_LIBRARIES)
|
||||
if (_wxinspector_interface)
|
||||
list(FILTER _wxinspector_interface EXCLUDE REGEX "wx(base|msw)3[0-9]u[_.]")
|
||||
set_target_properties(wxInspector::wxInspector PROPERTIES
|
||||
INTERFACE_LINK_LIBRARIES "${_wxinspector_interface}")
|
||||
endif ()
|
||||
|
||||
list(APPEND wxWidgets_LIBRARIES "wxInspector::wxInspector")
|
||||
|
||||
message(STATUS "wx libs: ${wxWidgets_LIBRARIES}")
|
||||
|
||||
+386
-66
@@ -53,6 +53,7 @@ using namespace nlohmann;
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/GCode.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
@@ -77,8 +78,9 @@ using namespace nlohmann;
|
||||
#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
|
||||
@@ -100,6 +102,10 @@ using namespace nlohmann;
|
||||
|
||||
#ifdef SLIC3R_GUI
|
||||
#include "slic3r/GUI/GUI_Init.hpp"
|
||||
// BBLPrinterAgent::from_orca_filament_id(); the map and its lookups live in libslic3r_gui,
|
||||
// which only a SLIC3R_GUI build links (see target_link_libraries(OrcaSlicer libslic3r_gui)
|
||||
// in CMakeLists).
|
||||
#include "slic3r/Utils/BBLPrinterAgent.hpp"
|
||||
#endif /* SLIC3R_GUI */
|
||||
|
||||
using namespace Slic3r;
|
||||
@@ -1462,6 +1468,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;
|
||||
@@ -1921,7 +1931,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);
|
||||
}
|
||||
@@ -1970,7 +1980,124 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
auto load_config_file = [](const std::string& file, DynamicPrintConfig& config, std::string& config_type,
|
||||
std::unique_ptr<PresetBundle> cli_preset_bundle;
|
||||
auto ensure_cli_preset_bundle = [&cli_preset_bundle](std::string &error) -> PresetBundle * {
|
||||
if (cli_preset_bundle)
|
||||
return cli_preset_bundle.get();
|
||||
try {
|
||||
AppConfig app_config;
|
||||
const std::string app_config_error = app_config.load_if_exists();
|
||||
if (!app_config_error.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Ignoring invalid app config during CLI preset resolution: " << app_config_error;
|
||||
app_config.reset();
|
||||
}
|
||||
|
||||
auto bundle = std::make_unique<PresetBundle>();
|
||||
std::string load_error;
|
||||
bundle->load_presets(app_config, config_substitution_rule,
|
||||
PresetBundle::PresetPreferences(), &load_error, true);
|
||||
if (!load_error.empty()) {
|
||||
error = "Failed to load presets for inheritance resolution: " + load_error;
|
||||
return nullptr;
|
||||
}
|
||||
cli_preset_bundle = std::move(bundle);
|
||||
return cli_preset_bundle.get();
|
||||
} catch (const std::exception &ex) {
|
||||
error = ex.what();
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
auto resolve_preset = [&ensure_cli_preset_bundle](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();
|
||||
allow_source_manifest = true;
|
||||
} else {
|
||||
bundle = ensure_cli_preset_bundle(error);
|
||||
if (bundle == nullptr)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (probe_type) {
|
||||
Preset::Type preset_type;
|
||||
if (!bundle->resolve_preset_config_type(config, preset_type, file, config_substitution_rule,
|
||||
error, allow_source_manifest))
|
||||
return false;
|
||||
config_type = Preset::get_type_string(preset_type);
|
||||
return true;
|
||||
}
|
||||
|
||||
Preset::Type preset_type;
|
||||
if (config_type == "process")
|
||||
preset_type = Preset::TYPE_PRINT;
|
||||
else if (config_type == "filament")
|
||||
preset_type = Preset::TYPE_FILAMENT;
|
||||
else if (config_type == "machine")
|
||||
preset_type = Preset::TYPE_PRINTER;
|
||||
else {
|
||||
error = "Unsupported preset type: " + config_type;
|
||||
return false;
|
||||
}
|
||||
return bundle->resolve_preset_config(config, preset_type, file, config_substitution_rule,
|
||||
error, allow_source_manifest);
|
||||
};
|
||||
|
||||
//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;
|
||||
@@ -1999,9 +2126,15 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
|
||||
auto type_iter = key_values.find(BBL_JSON_KEY_TYPE);
|
||||
if (type_iter != key_values.end()) {
|
||||
const bool probe_type = type_iter == key_values.end();
|
||||
if (!probe_type)
|
||||
config_type = type_iter->second;
|
||||
|
||||
if (!resolve_preset(file, config, config_type, config_from, probe_type, reason)) {
|
||||
boost::nowide::cerr << __FUNCTION__ << boost::format(": can not resolve preset %1%: %2%") % file % reason << std::endl;
|
||||
return CLI_CONFIG_FILE_ERROR;
|
||||
}
|
||||
|
||||
if (config_type == "machine") {
|
||||
//config.set("printer_settings_id", config_name, true);
|
||||
//printer_inherits = config.option<ConfigOptionString>("inherits", true)->value;
|
||||
@@ -2553,6 +2686,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2575,6 +2710,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2593,46 +2730,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");
|
||||
@@ -2855,8 +3034,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
|
||||
@@ -2998,8 +3179,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
|
||||
@@ -3186,6 +3373,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) {
|
||||
@@ -3197,8 +3394,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 {
|
||||
@@ -3649,9 +3846,94 @@ 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();
|
||||
|
||||
@@ -3933,7 +4215,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();
|
||||
@@ -3977,22 +4259,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);
|
||||
@@ -4753,13 +5026,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)
|
||||
@@ -4793,7 +5069,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);
|
||||
@@ -5056,7 +5332,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;
|
||||
@@ -5093,7 +5372,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) {
|
||||
@@ -5194,21 +5473,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, plate_height;
|
||||
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%")
|
||||
@@ -5724,6 +5998,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
|
||||
@@ -6538,6 +6840,20 @@ int CLI::run(int argc, char **argv)
|
||||
std::string nozzle_diameter_str;
|
||||
if (nozzle_diameter_option)
|
||||
nozzle_diameter_str = nozzle_diameter_option->serialize();
|
||||
#ifdef SLIC3R_GUI
|
||||
// A Bambu printer reads slice_info.config and knows only its own catalog ids. The GUI
|
||||
// gates the same translation on PresetBundle::is_bbl_vendor(); the CLI has no
|
||||
// PresetBundle, so reuse the printer_model prefix that already decides
|
||||
// Print::is_BBL_printer() for this same run.
|
||||
auto* printer_model_option = dynamic_cast<const ConfigOptionString*>(m_print_config.option("printer_model"));
|
||||
const bool is_bbl_printer = printer_model_option && printer_model_option->value.compare(0, 9, "Bambu Lab") == 0;
|
||||
// No wxApp on the CLI path, so there is no live agent to ask; the translator is stateless
|
||||
// over a lazily loaded map, so one instance serves every plate and filament below.
|
||||
// ORCA TODO: this assumes Bambu's is the only agent with a catalog of its own. Once another
|
||||
// agent carries one, resolve the agent from the selected printer the way
|
||||
// GUI_App::resolve_printer_agent_id does, rather than hard-coding BBLPrinterAgent here.
|
||||
const BBLPrinterAgent bbl_agent;
|
||||
#endif /* SLIC3R_GUI */
|
||||
|
||||
for (int i = 0; i < plate_data_list.size(); i++) {
|
||||
PlateData *plate_data = plate_data_list[i];
|
||||
@@ -6555,6 +6871,10 @@ int CLI::run(int argc, char **argv)
|
||||
it->type = m_print_config.get_filament_type(display_filament_type, it->id);
|
||||
it->color = (filament_color && !filament_color->values.empty()) ? filament_color->get_at(it->id) : "#FFFFFF";
|
||||
it->filament_id = (filament_id && !filament_id->values.empty()) ? filament_id->get_at(it->id) : "";
|
||||
#ifdef SLIC3R_GUI
|
||||
if (is_bbl_printer)
|
||||
it->filament_id = bbl_agent.from_orca_filament_id(it->filament_id);
|
||||
#endif /* SLIC3R_GUI */
|
||||
}
|
||||
|
||||
if (!plate_data->plate_thumbnail.is_valid()) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <boost/format.hpp>
|
||||
#include <mutex>
|
||||
|
||||
#include "git_commit_hash.h"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
static std::string g_log_folder;
|
||||
@@ -39,7 +40,7 @@ CBaseException::CBaseException(HANDLE hProcess, WORD wPID, LPCTSTR lpSymbolPath,
|
||||
output_file->open(log_filename, std::ios::out | std::ios::app);
|
||||
|
||||
// Output app build info in crash log so we could look for the correct PDB files
|
||||
OutputString(_T("%s\n\n"), _T(SLIC3R_APP_NAME " " SoftFever_VERSION " Build " GIT_COMMIT_HASH));
|
||||
OutputString(_T("%s\n\n"), _T(SLIC3R_APP_NAME " " SoftFever_VERSION " Build " GIT_COMMIT_HASH GIT_COMMIT_SUFFIX));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -175,6 +209,17 @@ void select_printer_default_presets(PresetBundle &bundle)
|
||||
if (const auto *def_fil = printer_preset.config.option<ConfigOptionStrings>("default_filament_profile");
|
||||
def_fil != nullptr && !def_fil->values.empty())
|
||||
bundle.filaments.select_preset_by_name(def_fil->values.front(), /*force=*/true);
|
||||
// Re-seed the per-slot filament list from that selection, or the sweep's result depends on the
|
||||
// printer sliced before it. Once there are 2+ slots, full_config() builds the filament config from
|
||||
// filament_presets and ignores the selected preset (PresetBundle::full_fff_config), while
|
||||
// update_compatible() only replaces a slot that has gone *incompatible* - and when it does, it ranks
|
||||
// the outgoing preset's alias, then its filament type, above the printer's own default. The sweep
|
||||
// grows every printer to 2 slots and update_multi_material_filament_presets() never shrinks them, so
|
||||
// a material picked up on the first printer rides the whole run. With all vendors loaded the first
|
||||
// printer inherits a TPU (the load-time pick is whichever filament sorts first), the type match
|
||||
// re-resolves it to "Generic TPU @System", and its alias then pins every later printer to that
|
||||
// vendor's own "Generic TPU @..." - which the BBL dual-nozzle profiles rightly refuse to group.
|
||||
bundle.filament_presets.assign(1, bundle.filaments.get_selected_preset_name());
|
||||
}
|
||||
|
||||
// The vendor/printer currently being sliced, stamped onto every engine log record by the sink below so
|
||||
@@ -381,7 +426,7 @@ int main(int argc, char* argv[])
|
||||
("generate_presets,g", po::value<bool>()->default_value(false), "Generate user presets for mock test")
|
||||
("slice,s", po::bool_switch()->default_value(false), "Slice a two-colour cube through every printer to expand all custom g-code (catches placeholder/flow errors that static checks miss). Off unless this flag is present.")
|
||||
("outdir,o", po::value<std::string>()->default_value(""), "With -s, also save each printer's g-code to this folder (as <vendor>__<printer>.gcode) for manual inspection. Optional.")
|
||||
("check_filament_subtypes,f", po::bool_switch()->default_value(false), "Also flag printers with duplicate (ambiguous) filament subtypes. Off unless this flag is present.")
|
||||
("check_filament_subtypes,f", po::bool_switch()->default_value(true), "Also flag printers with duplicate (ambiguous) filament subtypes. Off unless this flag is present.")
|
||||
("log_level,l", po::value<int>()->default_value(2), "Log level. Optional, default is 2 (warning). Higher values produce more detailed logs.");
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -635,6 +638,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");
|
||||
@@ -1815,7 +1823,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()
|
||||
@@ -1823,4 +1834,9 @@ bool AppConfig::exists()
|
||||
return boost::filesystem::exists(config_path());
|
||||
}
|
||||
|
||||
std::string AppConfig::load_if_exists()
|
||||
{
|
||||
return boost::filesystem::exists(loading_path()) ? load() : std::string();
|
||||
}
|
||||
|
||||
}; // namespace Slic3r
|
||||
|
||||
@@ -113,8 +113,10 @@ public:
|
||||
void set_defaults();
|
||||
|
||||
// Load the slic3r.ini from a user profile directory (or a datadir, if configured).
|
||||
// return error string or empty strinf
|
||||
// Return an error string, or an empty string on success.
|
||||
std::string load();
|
||||
// Treat a missing config as default state; otherwise load it normally.
|
||||
std::string load_if_exists();
|
||||
// Store the slic3r.ini into a user profile directory (or a datadir, if configured).
|
||||
void save();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "BlacklistedLibraryCheck.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/nowide/convert.hpp>
|
||||
|
||||
#ifdef WIN32
|
||||
|
||||
@@ -13,7 +13,6 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
|
||||
: m_bed_shape(printable_area), m_max_print_height(printable_height), m_extruder_shapes(extruder_areas), m_extruder_printable_height(extruder_printable_heights)
|
||||
{
|
||||
assert(printable_height >= 0);
|
||||
//assert(extruder_printable_heights.size() == extruder_areas.size());
|
||||
|
||||
m_polygon = Polygon::new_scale(printable_area);
|
||||
assert(m_polygon.is_counter_clockwise());
|
||||
@@ -86,6 +85,9 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
|
||||
m_shared_volume.data[2] = m_bboxf.max.x();
|
||||
m_shared_volume.data[3] = m_bboxf.max.y();
|
||||
m_shared_volume.zs[1] = m_bboxf.max.z();
|
||||
if (extruder_printable_heights.size() < m_extruder_shapes.size())
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("extruder_printable_height has only %1% entries but extruder_printable_area has %2%, falling back to the bed printable_height for the missing ones")
|
||||
% extruder_printable_heights.size() % m_extruder_shapes.size();
|
||||
for (unsigned int index = 0; index < m_extruder_shapes.size(); index++)
|
||||
{
|
||||
std::vector<Vec2d>& extruder_shape = m_extruder_shapes[index];
|
||||
@@ -100,7 +102,9 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
|
||||
return;
|
||||
}
|
||||
|
||||
if ((extruder_shape == printable_area)&&(extruder_printable_heights[index] == printable_height)) {
|
||||
const double extruder_height = index < extruder_printable_heights.size() ? extruder_printable_heights[index] : printable_height;
|
||||
|
||||
if ((extruder_shape == printable_area)&&(extruder_height == printable_height)) {
|
||||
extruder_volume.same_with_bed = true;
|
||||
extruder_volume.type = m_type;
|
||||
extruder_volume.bbox = m_bbox;
|
||||
@@ -113,7 +117,7 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
|
||||
double poly_area = poly.area();
|
||||
extruder_volume.bbox = get_extents(poly);
|
||||
BoundingBoxf temp_bboxf = get_extents(extruder_shape);
|
||||
extruder_volume.bboxf = BoundingBoxf3{ to_3d(temp_bboxf.min, 0.), to_3d(temp_bboxf.max, extruder_printable_heights[index]) };
|
||||
extruder_volume.bboxf = BoundingBoxf3{ to_3d(temp_bboxf.min, 0.), to_3d(temp_bboxf.max, extruder_height) };
|
||||
|
||||
if (extruder_shape.size() >= 4 && std::abs((poly_area - double(extruder_volume.bbox.size().x()) * double(extruder_volume.bbox.size().y()))) < sqr(SCALED_EPSILON))
|
||||
{
|
||||
|
||||
@@ -147,6 +147,8 @@ set(lisbslic3r_sources
|
||||
Fill/FillBase.hpp
|
||||
Fill/FillConcentric.cpp
|
||||
Fill/FillConcentric.hpp
|
||||
Fill/FillSpiralInset.cpp
|
||||
Fill/FillSpiralInset.hpp
|
||||
Fill/FillConcentricInternal.cpp
|
||||
Fill/FillConcentricInternal.hpp
|
||||
Fill/FillCornerSmoothing.cpp
|
||||
@@ -270,6 +272,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
|
||||
@@ -368,6 +372,8 @@ set(lisbslic3r_sources
|
||||
Preset.hpp
|
||||
PrincipalComponents2D.cpp
|
||||
PrincipalComponents2D.hpp
|
||||
PublishSettings.cpp
|
||||
PublishSettings.hpp
|
||||
PrintApply.cpp
|
||||
PrintBase.cpp
|
||||
PrintBase.hpp
|
||||
@@ -552,6 +558,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
|
||||
@@ -663,6 +675,13 @@ if(SLIC3R_PROFILE)
|
||||
target_link_libraries(libslic3r PRIVATE Shiny)
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
# Public, since BlacklistedLibraryCheck.hpp includes windows.h. Empty
|
||||
# WIN32_LEAN_AND_MEAN matches the sources that define it themselves; bare
|
||||
# NOMINMAX matches the one libigl already passes.
|
||||
target_compile_definitions(libslic3r PUBLIC "WIN32_LEAN_AND_MEAN=" "NOMINMAX")
|
||||
endif ()
|
||||
|
||||
if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY)
|
||||
add_precompiled_header(libslic3r pchheader.hpp FORCEINCLUDE)
|
||||
endif ()
|
||||
|
||||
@@ -1049,7 +1049,8 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
|
||||
std::vector<std::string>& different_settings = this->option<ConfigOptionStrings>("different_settings_to_system", true)->values;
|
||||
size_t size = different_settings.size();
|
||||
if (size == 0) {
|
||||
size = this->option<ConfigOptionStrings>("filament_settings_id")->values.size() + 2;
|
||||
const auto *filament_ids = this->option<ConfigOptionStrings>("filament_settings_id");
|
||||
size = (filament_ids ? filament_ids->values.size() : 0) + 2;
|
||||
different_settings.resize(size);
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -342,7 +342,7 @@ void fuzzy_polyline(Points& poly, bool closed, coordf_t slice_z, const FuzzySkin
|
||||
}
|
||||
|
||||
// Thanks Cura developers for this function.
|
||||
void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, const FuzzySkinConfig& cfg, bool closed)
|
||||
void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, coordf_t layer_height, const FuzzySkinConfig& cfg, bool closed)
|
||||
{
|
||||
|
||||
if (cfg.noise_type == NoiseType::Ripple) {
|
||||
@@ -356,7 +356,9 @@ void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice
|
||||
|
||||
const double min_dist_between_points = cfg.point_distance * 3. / 4.; // hardcoded: the point distance may vary between 3/4 and 5/4 the supplied value
|
||||
const double range_random_point_dist = cfg.point_distance / 2.;
|
||||
const double min_extrusion_width = 0.01; // workaround for many print options. Need overwrite formula with the layer height parameter. The width must more than >>> layer_height * (1 - 0.25 * PI) * 1.05 <<< (last num is the coeff of overlay error case)
|
||||
// ExtrusionJunction::w is a scaled coord_t, so this floor must be scaled too.
|
||||
// Flow::rounded_rectangle_extrusion_spacing() requires width > height * (1 - 0.25 * PI); keep 5% above it.
|
||||
const double min_extrusion_width = scaled<double>(layer_height * (1. - 0.25 * M_PI) * 1.05);
|
||||
double dist_left_over = random_value() * (min_dist_between_points / 2.); // the distance to be traversed on the line before making the first new point
|
||||
|
||||
auto* p0 = &ext_lines.front();
|
||||
@@ -685,12 +687,13 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim
|
||||
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour, const bool closed)
|
||||
{
|
||||
const auto slice_z = perimeter_generator.slice_z;
|
||||
const auto layer_height = perimeter_generator.layer_height;
|
||||
const auto& regions = perimeter_generator.regions_by_fuzzify;
|
||||
if (regions.size() == 1) { // optimization
|
||||
const auto& config = regions.begin()->first;
|
||||
const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour);
|
||||
if (fuzzify)
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, config, closed);
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, config, closed);
|
||||
} else {
|
||||
// Merge regions that produce identical fuzzy effects (differ only in type).
|
||||
// When the style (e.g. External) and a painted region (All) both fuzzify this loop
|
||||
@@ -701,7 +704,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
|
||||
|
||||
// Fast path: single merged region — apply directly without splitting
|
||||
if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty()) {
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, *merged_regions.front().config, closed);
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, *merged_regions.front().config, closed);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -761,7 +764,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
|
||||
// Fuzzy splitted extrusion
|
||||
if (std::all_of(splitted.begin(), splitted.end(), [](const Algorithm::SplitLineJunction& j) { return j.clipped; })) {
|
||||
// The entire polygon is fuzzified
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, *r.config, closed);
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, *r.config, closed);
|
||||
continue;
|
||||
} else {
|
||||
const auto current_ext = extrusion->junctions;
|
||||
@@ -769,12 +772,12 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
|
||||
segment.reserve(current_ext.size());
|
||||
extrusion->junctions.clear();
|
||||
|
||||
const auto fuzzy_current_segment = [&segment, &extrusion, &r, slice_z]() {
|
||||
const auto fuzzy_current_segment = [&segment, &extrusion, &r, slice_z, layer_height]() {
|
||||
// Orca: non fuzzy points to isolate fuzzy region
|
||||
const auto front = segment.front();
|
||||
const auto back = segment.back();
|
||||
|
||||
fuzzy_extrusion_line(segment, slice_z, *r.config, false);
|
||||
fuzzy_extrusion_line(segment, slice_z, layer_height, *r.config, false);
|
||||
// Orca: only add non fuzzy point if it's not in the extrusion closing point.
|
||||
if (!extrusion->junctions.empty() && extrusion->junctions.front().p != front.p) {
|
||||
extrusion->junctions.push_back(front);
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Slic3r::Feature::FuzzySkin {
|
||||
|
||||
void fuzzy_polyline(Points& poly, bool closed, coordf_t slice_z, const FuzzySkinConfig& cfg);
|
||||
|
||||
void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, const FuzzySkinConfig& cfg, bool closed = true);
|
||||
void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, coordf_t layer_height, const FuzzySkinConfig& cfg, bool closed = true);
|
||||
|
||||
void group_region_by_fuzzify(PerimeterGenerator& g);
|
||||
|
||||
|
||||
+40
-41
@@ -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"
|
||||
@@ -950,7 +950,7 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
params.extruder = region_config.internal_solid_filament_id;
|
||||
// Orca: forced fill order applies only to top/bottom surfaces filled with a
|
||||
// center-based pattern; everything else stays at Default to keep batching together.
|
||||
if (params.pattern == ipConcentric || params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral) {
|
||||
if (params.pattern == ipConcentric || params.pattern == ipSpiralInset || params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral) {
|
||||
if (params.extrusion_role == erTopSolidInfill)
|
||||
params.fill_order = region_config.top_surface_fill_order.value;
|
||||
else if (params.extrusion_role == erBottomSurface)
|
||||
@@ -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)
|
||||
{
|
||||
@@ -1332,7 +1359,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
||||
params.anchor_length = surface_fill.params.anchor_length;
|
||||
params.anchor_length_max = surface_fill.params.anchor_length_max;
|
||||
params.resolution = resolution;
|
||||
params.use_arachne = surface_fill.params.pattern == ipConcentric || surface_fill.params.pattern == ipConcentricInternal;
|
||||
params.use_arachne = surface_fill.params.pattern == ipConcentric || surface_fill.params.pattern == ipSpiralInset ||
|
||||
surface_fill.params.pattern == ipConcentricInternal;
|
||||
params.layer_height = layerm->layer()->height;
|
||||
params.lateral_lattice_angle_1 = surface_fill.params.lateral_lattice_angle_1;
|
||||
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
|
||||
@@ -1352,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;
|
||||
@@ -1388,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) {
|
||||
@@ -1515,6 +1507,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
|
||||
case ipCubic:
|
||||
case ipLine:
|
||||
case ipConcentric:
|
||||
case ipSpiralInset:
|
||||
case ipHoneycomb:
|
||||
case ipLateralHoneycomb:
|
||||
case ip3DHoneycomb:
|
||||
@@ -1581,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "FillBase.hpp"
|
||||
#include "FillConcentric.hpp"
|
||||
#include "FillSpiralInset.hpp"
|
||||
#include "FillHoneycomb.hpp"
|
||||
#include "Fill3DHoneycomb.hpp"
|
||||
#include "FillGyroid.hpp"
|
||||
@@ -41,6 +42,7 @@ Fill* Fill::new_from_type(const InfillPattern type)
|
||||
{
|
||||
switch (type) {
|
||||
case ipConcentric: return new FillConcentric();
|
||||
case ipSpiralInset: return new FillSpiralInset();
|
||||
case ipHoneycomb: return new FillHoneycomb();
|
||||
case ipLateralHoneycomb: return new FillLateralHoneycomb();
|
||||
case ip3DHoneycomb: return new Fill3DHoneycomb();
|
||||
@@ -2465,9 +2467,11 @@ void Fill::connect_base_support(Polylines &&infill_ordered, const std::vector<co
|
||||
#endif // INFILL_DEBUG_OUTPUT
|
||||
|
||||
const std::vector<SupportArcCost> arches = evaluate_support_arches(infill_ordered, graph, spacing, params);
|
||||
static const double cost_low = line_spacing * 1.3;
|
||||
static const double cost_high = line_spacing * 2.;
|
||||
static const double cost_veryhigh = line_spacing * 3.;
|
||||
// Must not be static: line_spacing varies per call (base vs interface fills differ),
|
||||
// and a static here would fix these to whichever call ran first, order depending on thread count.
|
||||
const double cost_low = line_spacing * 1.3;
|
||||
const double cost_high = line_spacing * 2.;
|
||||
const double cost_veryhigh = line_spacing * 3.;
|
||||
|
||||
{
|
||||
std::vector<const SupportArcCost*> selected;
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
#include "../ClipperUtils.hpp"
|
||||
#include "../ExPolygon.hpp"
|
||||
#include "../Surface.hpp"
|
||||
#include "../VariableWidth.hpp"
|
||||
#include "Arachne/WallToolPaths.hpp"
|
||||
|
||||
#include "FillSpiralInset.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Index of the corner the spiral should start at. Every following loop is split at the point nearest
|
||||
// the end of the one before it, so this choice propagates inwards and decides where the whole spiral
|
||||
// hands over from ring to ring. A tight corner is the worst place for it: there the next ring
|
||||
// retreats along the bisector by spacing/sin(angle), so the spiral has to strike out several spacings
|
||||
// to reach it instead of stepping across to a ring running parallel one spacing away.
|
||||
//
|
||||
// A right angle is taken first when the loop has one. It clips cleanly, since the trimming below
|
||||
// scales with 1/sin(angle) and so is at its shortest and least sensitive there, and it holds its
|
||||
// shape as the loop is offset inwards, which keeps the handover in the same place ring after ring.
|
||||
// Failing that the widest corner is the flattest stretch on offer, which is the next best handover.
|
||||
// A straight point is no corner at all and only turns up as an artefact of the offsetting, so it is
|
||||
// skipped.
|
||||
static int find_spiral_start_corner(const Polygon& loop)
|
||||
{
|
||||
const size_t n = loop.points.size();
|
||||
if (n < 3)
|
||||
return 0;
|
||||
|
||||
// cos(85 deg): a corner within five degrees of square counts as a right angle.
|
||||
static const double right_angle_cos = 0.08716;
|
||||
// cos(179 deg): anything flatter than this counts as a straight point rather than a corner.
|
||||
static const double straight_cos = -0.99985;
|
||||
|
||||
// Only convex corners qualify. A reflex corner spans the same angle between its two edges but
|
||||
// bulges the other way, so the next ring in steps away from it along the bisector instead of
|
||||
// hugging it, and starting there hands over across a long diagonal on every single ring. Loops
|
||||
// arrive counter-clockwise, in which case a convex corner turns left, but check the winding
|
||||
// rather than trust it. A closed loop always has at least one convex corner.
|
||||
const double convex_turn = loop.is_counter_clockwise() ? 1.0 : -1.0;
|
||||
|
||||
double best_right_cos = right_angle_cos;
|
||||
int best_right = -1;
|
||||
double best_wide_cos = 1.0;
|
||||
int best_wide = -1;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const Point& p_prev = loop.points[(i - 1 + n) % n];
|
||||
const Point& p = loop.points[i];
|
||||
const Point& p_next = loop.points[(i + 1) % n];
|
||||
|
||||
Vec2d e_in = (p - p_prev).cast<double>();
|
||||
Vec2d e_out = (p_next - p).cast<double>();
|
||||
double len1 = e_in.norm();
|
||||
double len2 = e_out.norm();
|
||||
if (len1 < 1e-6 || len2 < 1e-6)
|
||||
continue;
|
||||
if (convex_turn * (e_in.x() * e_out.y() - e_in.y() * e_out.x()) <= 0.0)
|
||||
continue;
|
||||
|
||||
// Cosine of the angle the two edges span at the corner: 1 at a spike, 0 square, -1 straight.
|
||||
double cos_val = -e_in.dot(e_out) / (len1 * len2);
|
||||
if (std::abs(cos_val) < best_right_cos) {
|
||||
best_right_cos = std::abs(cos_val);
|
||||
best_right = int(i);
|
||||
}
|
||||
if (cos_val > straight_cos && cos_val < best_wide_cos) {
|
||||
best_wide_cos = cos_val;
|
||||
best_wide = int(i);
|
||||
}
|
||||
}
|
||||
if (best_right >= 0)
|
||||
return best_right;
|
||||
// A loop smooth enough to have no corner at all, a circle say, hands over equally well anywhere.
|
||||
return best_wide < 0 ? 0 : best_wide;
|
||||
}
|
||||
|
||||
// Length to trim off the end of a loop so that it does not overlap the start of the next one.
|
||||
// The theoretical gap is distance/sin(alpha), alpha being the angle between the last segment of the
|
||||
// loop and the first segment of the next one.
|
||||
static double loop_clip_length(const Polyline& loop_path, const double gap)
|
||||
{
|
||||
const Point& p_prev = loop_path.points[loop_path.points.size() - 2];
|
||||
const Point& p_last = loop_path.points.back();
|
||||
const Point& p_next = loop_path.points[1];
|
||||
Vec2d v1 = (p_last - p_prev).cast<double>();
|
||||
Vec2d v2 = (p_next - p_last).cast<double>();
|
||||
if (v1.norm() < 1e-6 || v2.norm() < 1e-6)
|
||||
return gap;
|
||||
|
||||
double alpha = std::atan2(std::abs(v1.x() * v2.y() - v1.y() * v2.x()), v1.dot(v2));
|
||||
// Outside 45deg < alpha < 120deg the 1/sin(alpha) term would clip far too much, so fall back to the plain gap.
|
||||
return (alpha > M_PI / 4 && alpha < 2 * M_PI / 3) ? gap / std::sin(alpha) : gap;
|
||||
}
|
||||
|
||||
// The chaining below drives two kinds of loop: the plain offset polygons of the classic path, and
|
||||
// Arachne's variable width walls. These are the only four steps that differ between them. Widths run
|
||||
// two per segment, so every point added or removed takes a pair with it.
|
||||
static Polyline open_loop(const Polygon& loop, int start_index) { return loop.split_at_index(start_index); }
|
||||
|
||||
static ThickPolyline open_loop(const Arachne::ExtrusionLine& loop, int start_index)
|
||||
{
|
||||
ThickPolyline path = Arachne::to_thick_polyline(loop);
|
||||
// start_at_index() rotates a closed path, and wants it closed with a matching width at both ends.
|
||||
if (path.points.front() != path.points.back()) {
|
||||
const coordf_t w_first = path.width.front(), w_last = path.width.back();
|
||||
path.points.emplace_back(path.points.front());
|
||||
path.width.emplace_back(w_last);
|
||||
path.width.emplace_back(w_first);
|
||||
}
|
||||
path.start_at_index(start_index);
|
||||
return path;
|
||||
}
|
||||
|
||||
static void clip_path_end(Polyline& path, double distance) { path.clip_end(distance); }
|
||||
|
||||
static void clip_path_end(ThickPolyline& path, double distance)
|
||||
{
|
||||
// Polyline::clip_end() knows nothing about the widths, so walk back trimming the two together.
|
||||
while (distance > 0 && path.points.size() >= 2) {
|
||||
const Point last = path.points.back();
|
||||
const coordf_t w_end = path.width.back();
|
||||
path.points.pop_back();
|
||||
path.width.pop_back();
|
||||
const coordf_t w_start = path.width.back();
|
||||
path.width.pop_back();
|
||||
|
||||
const Vec2d v = (path.points.back() - last).cast<double>();
|
||||
const double len = v.norm();
|
||||
if (len > distance) {
|
||||
const double t = distance / len;
|
||||
path.points.emplace_back((last.cast<double>() + v * t).cast<coord_t>());
|
||||
path.width.emplace_back(w_start);
|
||||
path.width.emplace_back(w_start + (w_end - w_start) * (1.0 - t));
|
||||
return;
|
||||
}
|
||||
distance -= len;
|
||||
}
|
||||
path.clear();
|
||||
}
|
||||
|
||||
static void append_path(Polyline& dst, Polyline&& src) { dst.append(std::move(src)); }
|
||||
|
||||
static void append_path(ThickPolyline& dst, ThickPolyline&& src)
|
||||
{
|
||||
if (dst.empty()) {
|
||||
dst = std::move(src);
|
||||
return;
|
||||
}
|
||||
if (dst.points.back() == src.points.front()) {
|
||||
// Carrying straight on from the same point, so there is no run across to give a width to.
|
||||
src.points.erase(src.points.begin());
|
||||
src.width.erase(src.width.begin(), src.width.begin() + 2);
|
||||
} else {
|
||||
// The run across to the next loop tapers between the two ends it joins.
|
||||
const coordf_t w_from = dst.width.back(), w_to = src.width.front();
|
||||
dst.width.emplace_back(w_from);
|
||||
dst.width.emplace_back(w_to);
|
||||
}
|
||||
append(dst.points, std::move(src.points));
|
||||
append(dst.width, std::move(src.width));
|
||||
}
|
||||
|
||||
// The classic loops all carry the same width, so the innermost one of an island can still ring an
|
||||
// unfilled pin hole, which the spiral plugs by running into the middle. Arachne's walls widen to take
|
||||
// up whatever is left over, so there is nothing there to plug and the stub would only double back
|
||||
// over the wall that just filled it.
|
||||
static bool leaves_a_centre_hole(const Polygon&) { return true; }
|
||||
static bool leaves_a_centre_hole(const Arachne::ExtrusionLine&) { return false; }
|
||||
|
||||
static void append_path_point(Polyline& path, const Point& point) { path.points.emplace_back(point); }
|
||||
|
||||
static void append_path_point(ThickPolyline& path, const Point& point)
|
||||
{
|
||||
const coordf_t w = path.width.back();
|
||||
path.points.emplace_back(point);
|
||||
path.width.emplace_back(w);
|
||||
path.width.emplace_back(w);
|
||||
}
|
||||
|
||||
// Chain the loops of one surface into as few continuous spirals as its shape allows. The loops arrive
|
||||
// ordered outside in, depth first, each paired with its outline in loop_outlines; every decision here
|
||||
// is made on those outlines, so the two kinds of loop take exactly the same route.
|
||||
template<class LoopType, class PathType>
|
||||
static std::vector<PathType> generate_spiral_insets(const FillParams& params,
|
||||
const std::vector<const LoopType*>& loops,
|
||||
const Polygons& loop_outlines,
|
||||
const coord_t distance,
|
||||
const ExPolygon& original_expoly)
|
||||
{
|
||||
std::vector<PathType> output;
|
||||
PathType spiral;
|
||||
Point current_pos(0, 0);
|
||||
// Index into loops of the innermost loop appended to the spiral currently being built.
|
||||
int innermost_loop = -1;
|
||||
|
||||
// Whether the spiral can run straight from one point to the other. The run across is extruded,
|
||||
// not travelled, so it has to be a genuine step over to the ring alongside:
|
||||
// - up to a ring spacing and a half it cannot leave the material, and needs no check at all,
|
||||
// which covers all but a few of the loops;
|
||||
// - beyond that it is tested against the surface, which catches the points that are close in a
|
||||
// straight line but separated by a hole or a notch;
|
||||
// - past four spacings it is refused outright. A handover does stretch at a corner, where the
|
||||
// next ring retreats along the bisector by spacing/sin(angle), but four spacings is already a
|
||||
// fifteen degree wedge, and down a wedge that tight the run across would trace the bisector,
|
||||
// which is where the tail is filled from anyway. Anything longer is a traverse across the
|
||||
// surface that prints over what it crosses. Breaking the spiral leaves the G-code to travel it.
|
||||
const double free_hop = 1.5 * double(distance);
|
||||
const double max_hop = 4.0 * double(distance);
|
||||
auto reachable = [&](const Point& from, const Point& to) {
|
||||
const double hop = from.distance_to(to);
|
||||
if (hop > max_hop)
|
||||
return false;
|
||||
return hop <= free_hop || original_expoly.contains(Line(from, to));
|
||||
};
|
||||
|
||||
// The centre point plugs the pin hole left in the middle of an island, it is not meant to
|
||||
// traverse it, so it is only worth adding when the innermost loop has shrunk to about a ring.
|
||||
const double max_center_stub = 2.0 * double(distance);
|
||||
|
||||
// Emit the spiral built so far as one path and start over on a fresh island.
|
||||
auto flush_spiral = [&]() {
|
||||
if (spiral.empty())
|
||||
return;
|
||||
// Run into the middle of the innermost loop so the island's centre is filled instead of being
|
||||
// left as a pin hole. Only where there is a hole to fill: the loop has to still enclose open
|
||||
// space once its own bead is accounted for, or the stub just runs back over that bead. And
|
||||
// the point has to sit inside the loop and be reachable, or it runs off across the surface.
|
||||
if (innermost_loop >= 0 && leaves_a_centre_hole(*loops[innermost_loop])) {
|
||||
const Polygon& innermost = loop_outlines[innermost_loop];
|
||||
const Point centroid = innermost.centroid();
|
||||
if (!offset(innermost, -float(0.5 * double(distance))).empty() && centroid != spiral.last_point() &&
|
||||
spiral.last_point().distance_to(centroid) <= max_center_stub && innermost.contains(centroid) &&
|
||||
reachable(spiral.last_point(), centroid))
|
||||
append_path_point(spiral, centroid);
|
||||
}
|
||||
output.emplace_back(std::move(spiral));
|
||||
spiral.clear();
|
||||
innermost_loop = -1;
|
||||
current_pos = Point(0, 0);
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < loops.size(); ++i) {
|
||||
const Polygon& outline = loop_outlines[i];
|
||||
if (outline.points.empty())
|
||||
continue;
|
||||
|
||||
// The loop is opened into a path with the split point repeated at both ends, so a usable one
|
||||
// has at least 3 points. Both kinds of loop share the outline's indices, hence its start point.
|
||||
PathType loop_path = open_loop(*loops[i], spiral.empty() ? find_spiral_start_corner(outline) :
|
||||
current_pos.nearest_point_index(outline.points));
|
||||
if (loop_path.size() < 3)
|
||||
continue;
|
||||
|
||||
// Island jumping: the loops are ordered by their nesting, depth first, so the next one
|
||||
// continues the current spiral exactly when it lies inside the one just laid down. Distance
|
||||
// cannot stand in for that test: at a sharp corner the next ring retreats along the bisector
|
||||
// by spacing/sin(angle), which leaves it several spacings away while still being the very
|
||||
// next ring in, and the spiral would break off at every spike.
|
||||
const bool same_island = innermost_loop >= 0 && loop_outlines[innermost_loop].contains(loop_path.points.front());
|
||||
if (!spiral.empty() && (!same_island || !reachable(spiral.last_point(), loop_path.points.front()))) {
|
||||
flush_spiral();
|
||||
loop_path = open_loop(*loops[i], find_spiral_start_corner(outline));
|
||||
if (loop_path.size() < 3)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clip the end of the loop to leave room for the run into the next one. The last loop of the
|
||||
// surface has no successor, so it only gives up half of the gap.
|
||||
clip_path_end(loop_path, loop_clip_length(loop_path, (i + 1 == loops.size() ? 0.5 : 1.0) * double(distance)));
|
||||
// Clipping empties the path when the loop is shorter than the clipping length, which happens
|
||||
// on the degenerate slivers that offsetting leaves behind. Such a loop carries no extrusion.
|
||||
if (loop_path.size() < 2)
|
||||
continue;
|
||||
|
||||
append_path(spiral, std::move(loop_path));
|
||||
innermost_loop = int(i);
|
||||
current_pos = spiral.last_point();
|
||||
}
|
||||
|
||||
flush_spiral();
|
||||
|
||||
// An outward fill order runs every spiral from its centre to its outer edge, innermost island first.
|
||||
if (params.fill_order != SurfaceFillOrder::Inward) {
|
||||
for (PathType& path : output)
|
||||
path.reverse();
|
||||
std::reverse(output.begin(), output.end());
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
void FillSpiralInset::_fill_surface_single(const FillParams& params,
|
||||
unsigned int thickness_layers,
|
||||
const std::pair<float, Point>& direction,
|
||||
ExPolygon expolygon,
|
||||
Polylines& polylines_out)
|
||||
{
|
||||
BoundingBox bounding_box = expolygon.contour.bounding_box();
|
||||
|
||||
coord_t min_spacing = scale_(this->spacing);
|
||||
coord_t distance = coord_t(min_spacing / params.density);
|
||||
|
||||
if (params.density > 0.9999f && !params.dont_adjust) {
|
||||
distance = this->_adjust_solid_spacing(bounding_box.size()(0), distance);
|
||||
this->spacing = unscale<double>(distance);
|
||||
}
|
||||
|
||||
Polygons loops = to_polygons(expolygon);
|
||||
|
||||
ExPolygons last{std::move(expolygon)};
|
||||
while (!last.empty()) {
|
||||
last = offset2_ex(last, -(distance + min_spacing / 2), +min_spacing / 2);
|
||||
append(loops, to_polygons(last));
|
||||
}
|
||||
|
||||
// Orders the loops outside in, depth first, which is the order the chaining below expects.
|
||||
loops = union_pt_chained_outside_in(loops);
|
||||
|
||||
std::vector<const Polygon*> loop_refs;
|
||||
loop_refs.reserve(loops.size());
|
||||
for (const Polygon& loop : loops)
|
||||
loop_refs.emplace_back(&loop);
|
||||
|
||||
Polylines spiral_result = generate_spiral_insets<Polygon, Polyline>(params, loop_refs, loops, distance, expolygon);
|
||||
|
||||
append(polylines_out, spiral_result);
|
||||
}
|
||||
|
||||
void FillSpiralInset::_fill_surface_single(const FillParams& params,
|
||||
unsigned int thickness_layers,
|
||||
const std::pair<float, Point>& direction,
|
||||
ExPolygon expolygon,
|
||||
ThickPolylines& thick_polylines_out)
|
||||
{
|
||||
assert(params.use_arachne);
|
||||
assert(this->print_config != nullptr && this->print_object_config != nullptr);
|
||||
|
||||
// Only a solid surface is worth the variable width walls; a sparse one falls back to plain loops.
|
||||
if (params.density <= 0.9999f || params.dont_adjust) {
|
||||
Polylines polylines;
|
||||
this->_fill_surface_single(params, thickness_layers, direction, expolygon, polylines);
|
||||
append(thick_polylines_out, to_thick_polylines(std::move(polylines), scaled<coord_t>(this->spacing)));
|
||||
return;
|
||||
}
|
||||
|
||||
// no rotation is supported for this infill pattern
|
||||
Point bbox_size = expolygon.contour.bounding_box().size();
|
||||
coord_t min_spacing = scaled<coord_t>(this->spacing);
|
||||
|
||||
coord_t loops_count = std::max(bbox_size.x(), bbox_size.y()) / min_spacing + 1;
|
||||
Polygons polygons = offset(expolygon, float(min_spacing) / 2.f);
|
||||
|
||||
double min_nozzle_diameter = *std::min_element(print_config->nozzle_diameter.values.begin(), print_config->nozzle_diameter.values.end());
|
||||
Arachne::WallToolPathsParams input_params;
|
||||
input_params.min_bead_width = 0.85 * min_nozzle_diameter;
|
||||
input_params.min_feature_size = 0.25 * min_nozzle_diameter;
|
||||
input_params.wall_transition_length = 1.0 * min_nozzle_diameter;
|
||||
input_params.wall_transition_angle = 10;
|
||||
input_params.wall_transition_filter_deviation = 0.25 * min_nozzle_diameter;
|
||||
input_params.wall_distribution_count = 1;
|
||||
|
||||
Arachne::WallToolPaths wallToolPaths(polygons, min_spacing, min_spacing, loops_count, 0, params.layer_height, input_params);
|
||||
std::vector<Arachne::VariableWidthLines> walls_by_inset = wallToolPaths.getToolPaths();
|
||||
|
||||
// Open walls are the thin features Arachne fits between the closed ones. They cannot join a
|
||||
// spiral, so they go out as they are; leaving them behind is what would put the gaps back.
|
||||
std::vector<const Arachne::ExtrusionLine*> walls;
|
||||
Polygons wall_outlines;
|
||||
ThickPolylines open_walls;
|
||||
for (const Arachne::VariableWidthLines& inset : walls_by_inset)
|
||||
for (const Arachne::ExtrusionLine& wall : inset) {
|
||||
if (wall.empty())
|
||||
continue;
|
||||
if (wall.is_closed) {
|
||||
walls.emplace_back(&wall);
|
||||
wall_outlines.emplace_back(wall.toPolygon());
|
||||
} else {
|
||||
open_walls.emplace_back(Arachne::to_thick_polyline(wall));
|
||||
}
|
||||
}
|
||||
|
||||
// Arachne hands the walls back grouped by inset, which is not their nesting: around a hole the
|
||||
// wall of a given inset lies inside the wall of that same inset around the contour. Nest them by
|
||||
// containment instead, so the spiral follows one island all the way in before starting the next,
|
||||
// the same order union_pt_chained_outside_in gives the classic path above.
|
||||
const size_t wall_count = walls.size();
|
||||
std::vector<int> nesting_depth(wall_count, 0), parent(wall_count, -1);
|
||||
std::vector<char> inside(wall_count * wall_count, 0);
|
||||
for (size_t i = 0; i < wall_count; ++i)
|
||||
for (size_t j = 0; j < wall_count; ++j)
|
||||
if (i != j && wall_outlines[j].contains(walls[i]->junctions.front().p)) {
|
||||
inside[i * wall_count + j] = 1;
|
||||
++nesting_depth[i];
|
||||
}
|
||||
// The innermost of the walls containing this one, which is the deepest of them, is its parent.
|
||||
for (size_t i = 0; i < wall_count; ++i)
|
||||
for (size_t j = 0; j < wall_count; ++j)
|
||||
if (inside[i * wall_count + j] && (parent[i] < 0 || nesting_depth[parent[i]] < nesting_depth[j]))
|
||||
parent[i] = int(j);
|
||||
|
||||
std::vector<const Arachne::ExtrusionLine*> ordered;
|
||||
Polygons outlines;
|
||||
ordered.reserve(wall_count);
|
||||
outlines.reserve(wall_count);
|
||||
std::function<void(int)> descend = [&](int idx) {
|
||||
ordered.emplace_back(walls[idx]);
|
||||
outlines.emplace_back(wall_outlines[idx]);
|
||||
for (size_t k = 0; k < wall_count; ++k)
|
||||
if (parent[k] == idx)
|
||||
descend(int(k));
|
||||
};
|
||||
for (size_t i = 0; i < wall_count; ++i)
|
||||
if (parent[i] < 0)
|
||||
descend(int(i));
|
||||
|
||||
ThickPolylines spiral_result =
|
||||
generate_spiral_insets<Arachne::ExtrusionLine, ThickPolyline>(params, ordered, outlines, min_spacing, expolygon);
|
||||
|
||||
append(thick_polylines_out, std::move(spiral_result));
|
||||
append(thick_polylines_out, std::move(open_walls));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef slic3r_FillSpiralInset_hpp_
|
||||
#define slic3r_FillSpiralInset_hpp_
|
||||
|
||||
#include "FillBase.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class FillSpiralInset : public Fill
|
||||
{
|
||||
public:
|
||||
~FillSpiralInset() override = default;
|
||||
bool is_self_crossing() override { return false; }
|
||||
|
||||
protected:
|
||||
Fill* clone() const override { return new FillSpiralInset(*this); };
|
||||
void _fill_surface_single(
|
||||
const FillParams ¶ms,
|
||||
unsigned int thickness_layers,
|
||||
const std::pair<float, Point> &direction,
|
||||
ExPolygon expolygon,
|
||||
Polylines &polylines_out) override;
|
||||
|
||||
// Orca: solid surfaces are filled with Arachne's variable width walls, which widen to take up
|
||||
// whatever the fixed width loops above would have left over as gaps.
|
||||
void _fill_surface_single(
|
||||
const FillParams ¶ms,
|
||||
unsigned int thickness_layers,
|
||||
const std::pair<float, Point> &direction,
|
||||
ExPolygon expolygon,
|
||||
ThickPolylines &thick_polylines_out) override;
|
||||
|
||||
bool no_sort() const override { return true; }
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_FillSpiralInset_hpp_
|
||||
@@ -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.
|
||||
@@ -681,6 +642,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;
|
||||
@@ -1217,6 +1183,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);
|
||||
@@ -2041,7 +2021,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");
|
||||
@@ -3605,7 +3585,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)
|
||||
@@ -5328,7 +5308,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
|
||||
@@ -5974,6 +5954,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;
|
||||
@@ -6073,6 +6054,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;
|
||||
|
||||
@@ -6482,7 +6464,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; }
|
||||
@@ -6495,8 +6480,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);
|
||||
|
||||
@@ -6968,10 +6953,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);
|
||||
@@ -6994,7 +7000,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";
|
||||
}
|
||||
@@ -8834,7 +8844,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) {
|
||||
@@ -8844,7 +8854,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();
|
||||
}
|
||||
|
||||
@@ -8862,7 +8872,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;
|
||||
@@ -9077,7 +9087,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())
|
||||
@@ -9173,6 +9183,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);
|
||||
|
||||
|
||||
@@ -6308,8 +6308,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -3137,7 +3137,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.
|
||||
|
||||
@@ -1630,6 +1630,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};
|
||||
@@ -4883,12 +4971,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -125,7 +125,6 @@ public:
|
||||
/******************************************** Splitting the 128bit number into two 64bit words *********************************************/
|
||||
|
||||
Int128(int64_t lo = 0) : m_lo((uint64_t)lo), m_hi((lo < 0) ? -1 : 0) {}
|
||||
Int128(const Int128 &val) : m_lo(val.m_lo), m_hi(val.m_hi) {}
|
||||
Int128(const int64_t& hi, const uint64_t& lo) : m_lo(lo), m_hi(hi) {}
|
||||
|
||||
Int128& operator = (const int64_t &val)
|
||||
|
||||
+16
-7
@@ -187,6 +187,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 +223,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 +235,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();
|
||||
@@ -419,6 +427,7 @@ coordf_t Layer::get_sparse_infill_max_void_area()
|
||||
double spacing = flow.scaled_spacing() * (100 - density) / density;
|
||||
switch (pattern) {
|
||||
case ipConcentric:
|
||||
case ipSpiralInset:
|
||||
case ipRectilinear:
|
||||
case ipLine:
|
||||
case ipGyroid:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -3598,6 +3598,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,
|
||||
@@ -3862,6 +3871,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(); }
|
||||
@@ -1790,6 +1794,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);
|
||||
|
||||
@@ -24,6 +24,7 @@ public:
|
||||
explicit MultiPoint(const Points &_points) : points(_points) {}
|
||||
MultiPoint& operator=(const MultiPoint &other) { points = other.points; return *this; }
|
||||
MultiPoint& operator=(MultiPoint &&other) { points = std::move(other.points); return *this; }
|
||||
virtual ~MultiPoint() = default;
|
||||
void scale(double factor);
|
||||
void scale(double factor_x, double factor_y);
|
||||
void translate(double x, double y) { this->translate(Point(coord_t(x), coord_t(y))); }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2127,7 +2127,7 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
|
||||
bridgeable_filtered = union_ex(offset_ex(remaining, perimeter_spacing), bridgeable_filtered);
|
||||
bridgeable_filtered = offset_ex(bridgeable_filtered, -perimeter_spacing);
|
||||
bridgeable_filtered = diff_ex(bridgeable_filtered, remaining, ApplySafetyOffset::Yes);
|
||||
bridgeable_filtered = opening_ex(bridgeable_filtered, perimeter_spacing); // filter noise from the diff_ex
|
||||
bridgeable_filtered = opening_ex(bridgeable_filtered, ext_perimeter_width / 2); // filter noise from the diff_ex
|
||||
bridgeable_filtered = offset_ex(bridgeable_filtered, perimeter_spacing); // restore the size to the original bridgeable area
|
||||
// Safety measure: Keep the bridge mask from intruding deeper into the
|
||||
// supported anchor region than the explicit anchor overlap.
|
||||
|
||||
+96
-11
@@ -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/assign_vendor_setting_ids.py;
|
||||
// namespace (OrcaCloudServiceAgent). Keep in sync with scripts/orca_id_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
|
||||
@@ -1653,7 +1667,7 @@ std::string PresetCollection::canonical_preset_name(const std::string &name, con
|
||||
void PresetCollection::load_presets(
|
||||
const std::string &dir_path, const std::string &subdir,
|
||||
PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule substitution_rule,
|
||||
std::function<void(Preset&)> preset_loaded_fn, const PresetOrigin &load_origin)
|
||||
std::function<void(Preset&)> preset_loaded_fn, const PresetOrigin &load_origin, bool read_only)
|
||||
{
|
||||
// Don't use boost::filesystem::canonical() on Windows, it is broken in regard to reparse points,
|
||||
// see https://github.com/prusa3d/PrusaSlicer/issues/732
|
||||
@@ -1662,7 +1676,7 @@ void PresetCollection::load_presets(
|
||||
|
||||
// Load custom roots first
|
||||
if (fs::exists(dir / "base")) {
|
||||
load_presets(dir.string(), "base", substitutions, substitution_rule, nullptr, resolved_origin);
|
||||
load_presets(dir.string(), "base", substitutions, substitution_rule, nullptr, resolved_origin, read_only);
|
||||
}
|
||||
|
||||
//BBS: add config related logs
|
||||
@@ -1670,7 +1684,8 @@ void PresetCollection::load_presets(
|
||||
//BBS do not parse folder if not exists
|
||||
m_dir_path = dir.string();
|
||||
if (!fs::exists(dir)) {
|
||||
fs::create_directory(dir);
|
||||
if (!read_only)
|
||||
fs::create_directory(dir);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1720,10 +1735,10 @@ void PresetCollection::load_presets(
|
||||
substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
|
||||
if (!reason.empty()) {
|
||||
fs::path file_path(preset.file);
|
||||
if (fs::exists(file_path))
|
||||
if (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
file_path.replace_extension(".info");
|
||||
if (fs::exists(file_path))
|
||||
if (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file;
|
||||
++m_errors;
|
||||
@@ -1794,7 +1809,8 @@ void PresetCollection::load_presets(
|
||||
size_t at_pos = name.find('@');
|
||||
if (at_pos != std::string::npos && at_pos + 1 < name.length()) {
|
||||
compatible_printers->values.push_back(name.substr(at_pos + 1));
|
||||
preset.save(nullptr);
|
||||
if (!read_only)
|
||||
preset.save(nullptr);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << name;
|
||||
}
|
||||
}
|
||||
@@ -1812,10 +1828,10 @@ void PresetCollection::load_presets(
|
||||
++m_errors;
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what();
|
||||
fs::path file_path(preset.file);
|
||||
if (fs::exists(file_path))
|
||||
if (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
file_path.replace_extension(".info");
|
||||
if (fs::exists(file_path))
|
||||
if (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
|
||||
} catch (const std::runtime_error &err) {
|
||||
@@ -1823,10 +1839,10 @@ void PresetCollection::load_presets(
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what();
|
||||
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
|
||||
fs::path file_path(preset.file);
|
||||
if (fs::exists(file_path))
|
||||
if (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
file_path.replace_extension(".info");
|
||||
if (fs::exists(file_path))
|
||||
if (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
}
|
||||
|
||||
@@ -3060,6 +3076,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/assign_vendor_setting_ids.py and recomputed here when a profile ships
|
||||
// without it. MUST stay byte-identical to scripts/assign_vendor_setting_ids.py.
|
||||
// scripts/orca_id_tool.py and recomputed here when a profile ships without it.
|
||||
// MUST stay byte-identical to scripts/orca_id_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 {
|
||||
@@ -558,7 +563,7 @@ public:
|
||||
void add_default_preset(const std::vector<std::string> &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &preset_name);
|
||||
|
||||
// Load ini files of the particular type from the provided directory path.
|
||||
void load_presets(const std::string &dir_path, const std::string &subdir, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule, std::function<void(Preset&)> preset_loaded_fn = nullptr, const PresetOrigin &load_origin = PresetOrigin());
|
||||
void load_presets(const std::string &dir_path, const std::string &subdir, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule, std::function<void(Preset&)> preset_loaded_fn = nullptr, const PresetOrigin &load_origin = PresetOrigin(), bool read_only = false);
|
||||
|
||||
//BBS: update user presets directory
|
||||
void update_user_presets_directory(const std::string& dir_path, const std::string& type);
|
||||
@@ -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.
|
||||
|
||||
+1737
-178
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,11 @@
|
||||
#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 <unordered_map>
|
||||
@@ -168,6 +170,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
|
||||
{
|
||||
@@ -230,7 +256,22 @@ public:
|
||||
// Load selections (current print, current filaments, current printer) from config.ini
|
||||
// select preferred presets, if any exist
|
||||
PresetsConfigSubstitutions load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule rule,
|
||||
const PresetPreferences& preferred_selection = PresetPreferences());
|
||||
const PresetPreferences& preferred_selection = PresetPreferences(),
|
||||
std::string *errors = nullptr, bool read_only = false);
|
||||
|
||||
// Resolve an explicitly named source file through a canonical flattened
|
||||
// preset. Exact loaded-file identity is preferred; otherwise a manifest-
|
||||
// backed vendor tree is loaded from that source root without using caches.
|
||||
bool resolve_preset_config(DynamicPrintConfig &config, Preset::Type type,
|
||||
const std::string &source_file,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
||||
std::string &error, bool allow_source_manifest = true);
|
||||
// Resolve a source file whose JSON omits `type`. Succeeds only when exactly
|
||||
// one FFF preset collection owns the file and returns that collection's type.
|
||||
bool resolve_preset_config_type(DynamicPrintConfig &config, Preset::Type &type,
|
||||
const std::string &source_file,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
||||
std::string &error, bool allow_source_manifest = true);
|
||||
|
||||
// Load selections (current print, current filaments, current printer) from config.ini
|
||||
// This is done just once on application start up.
|
||||
@@ -238,7 +279,7 @@ public:
|
||||
void load_selections(AppConfig &config, const PresetPreferences& preferred_selection = PresetPreferences());
|
||||
|
||||
// BBS Load user presets
|
||||
PresetsConfigSubstitutions load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule rule);
|
||||
PresetsConfigSubstitutions load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule rule, bool read_only = false);
|
||||
PresetsConfigSubstitutions load_user_presets(AppConfig &config, std::map<std::string, std::map<std::string, std::string>>& my_presets, ForwardCompatibilitySubstitutionRule rule);
|
||||
// Orca: Import subscribed bundle presets (load and save to disk in one operation), handles one bundle at a time
|
||||
PresetsConfigSubstitutions update_subscribed_presets(AppConfig& config,
|
||||
@@ -350,6 +391,13 @@ public:
|
||||
std::vector<std::vector<DynamicPrintConfig>> get_extruder_filament_info() const;
|
||||
|
||||
std::set<std::string> get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only = true);
|
||||
// Orca: the root filament presets a connected machine can use, resolved with the rule the rest
|
||||
// of the app applies (is_compatible_with_printer): an empty compatible_printers means every
|
||||
// printer, minus the alias shadowing exclusions the Orca Filament Library records in
|
||||
// Preset::m_excluded_from.
|
||||
std::vector<Preset *> get_filament_presets_for_machine(const std::string &printer_type,
|
||||
const std::string &nozzle_diameter_str,
|
||||
bool include_user_presets);
|
||||
bool check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray(const std::string &printer_type,
|
||||
std::string & nozzle_diameter_str,
|
||||
std::string & setting_id,
|
||||
@@ -442,8 +490,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.
|
||||
@@ -474,10 +522,13 @@ public:
|
||||
//Orca: load config bundle from json, pass the base bundle to support cross vendor inheritance
|
||||
// Orca: `dir` is where the vendor is looked for — its own directory, whether or
|
||||
// not the profile JSONs are still there. A whole-vendor load comes from the
|
||||
// vendor's preset cache whenever one covers the profile on disk, and is parsed
|
||||
// from the JSONs in `dir` only when none does. Nothing here reads resources.
|
||||
// vendor's preset cache whenever one covers the profile on disk and allow_cache
|
||||
// is true, and is parsed from the JSONs in `dir` otherwise. Nothing here reads
|
||||
// resources implicitly.
|
||||
std::pair<PresetsConfigSubstitutions, size_t> load_vendor_configs_from_json(
|
||||
const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr);
|
||||
const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr,
|
||||
bool allow_cache = true);
|
||||
|
||||
// Export a config bundle file containing all the presets and the names of the active presets.
|
||||
//void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false);
|
||||
@@ -599,6 +650,7 @@ private:
|
||||
|
||||
// Whether to (re)write a per-vendor cache after a JSON parse.
|
||||
bool m_generate_vendor_caches { false };
|
||||
bool m_preserve_vendor_source_paths { false };
|
||||
|
||||
// Orca: validation only - flag any printer with two or more compatible
|
||||
// filament presets sharing one filament_id (ambiguous AMS subtype match).
|
||||
@@ -606,7 +658,7 @@ private:
|
||||
|
||||
//std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule);
|
||||
//BBS: add json related logic
|
||||
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule);
|
||||
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache = true);
|
||||
// Update the multicolor information for filaments.
|
||||
void update_filament_multi_color();
|
||||
// Update renamed_from and alias maps of system profiles.
|
||||
@@ -620,7 +672,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);*/
|
||||
|
||||
|
||||
+109
-98
@@ -1,3 +1,8 @@
|
||||
#ifdef _WIN32
|
||||
// Keep this first. A header below reaches boost/regex, whose w32_regex_traits
|
||||
// needs the Win32 types declared already.
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
#include "Config.hpp"
|
||||
#include "Exception.hpp"
|
||||
#include "Print.hpp"
|
||||
@@ -15,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"
|
||||
@@ -1026,20 +1032,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()) {
|
||||
@@ -1061,36 +1068,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 {};
|
||||
}
|
||||
|
||||
@@ -3992,74 +4017,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;
|
||||
}
|
||||
|
||||
@@ -4285,6 +4261,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();
|
||||
@@ -4398,6 +4375,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();
|
||||
@@ -4433,7 +4411,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
|
||||
@@ -4446,6 +4426,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
|
||||
@@ -5994,17 +5996,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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -275,6 +275,7 @@ static t_config_enum_values s_keys_map_InfillPattern {
|
||||
{ "tpmsfk", ipTpmsFK },
|
||||
{ "gyroid", ipGyroid },
|
||||
{ "concentric", ipConcentric },
|
||||
{ "spiralinset", ipSpiralInset },
|
||||
{ "hilbertcurve", ipHilbertCurve },
|
||||
{ "archimedeanchords", ipArchimedeanChords },
|
||||
{ "octagramspiral", ipOctagramSpiral }
|
||||
@@ -371,6 +372,7 @@ static t_config_enum_values s_keys_map_SupportMaterialInterfacePattern {
|
||||
{ "auto", smipAuto },
|
||||
{ "rectilinear", smipRectilinear },
|
||||
{ "concentric", smipConcentric },
|
||||
{ "spiralinset", smipSpiralInset },
|
||||
{ "rectilinear_interlaced", smipRectilinearInterlaced},
|
||||
{ "grid", smipGrid }
|
||||
};
|
||||
@@ -2292,6 +2294,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_values.push_back("rectilinear");
|
||||
def->enum_values.push_back("alignedrectilinear");
|
||||
def->enum_values.push_back("concentric");
|
||||
def->enum_values.push_back("spiralinset");
|
||||
def->enum_values.push_back("hilbertcurve");
|
||||
def->enum_values.push_back("archimedeanchords");
|
||||
def->enum_values.push_back("octagramspiral");
|
||||
@@ -2300,6 +2303,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_labels.push_back(L("Rectilinear"));
|
||||
def->enum_labels.push_back(L("Aligned Rectilinear"));
|
||||
def->enum_labels.push_back(L("Concentric"));
|
||||
def->enum_labels.push_back(L("Spiral Inset"));
|
||||
def->enum_labels.push_back(L("Hilbert Curve"));
|
||||
def->enum_labels.push_back(L("Archimedean Chords"));
|
||||
def->enum_labels.push_back(L("Octagram Spiral"));
|
||||
@@ -2382,7 +2386,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->label = L("Top surface fill order");
|
||||
def->category = L("Strength");
|
||||
def->tooltip = L("Direction in which top surfaces are filled when using a center-based pattern "
|
||||
"(Concentric, Archimedean Chords, Octagram Spiral).\n"
|
||||
"(Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n"
|
||||
"Outward starts at the center of the surface, so any excess material is pushed "
|
||||
"towards the edge where it is least visible. Inward starts at the edge and ends "
|
||||
"with the tight curves at the center.\n"
|
||||
@@ -2401,7 +2405,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->label = L("Bottom surface fill order");
|
||||
def->category = L("Strength");
|
||||
def->tooltip = L("Direction in which bottom surfaces are filled when using a center-based pattern "
|
||||
"(Concentric, Archimedean Chords, Octagram Spiral).\n"
|
||||
"(Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n"
|
||||
"Inward starts each surface with the wider outer curves, which improves first layer "
|
||||
"adhesion on build plates where the tight curves at the center may not stick. "
|
||||
"Outward starts at the center, pushing any excess material towards the edge.\n"
|
||||
@@ -5426,7 +5430,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");
|
||||
@@ -6963,11 +6967,13 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_values.push_back("auto");
|
||||
def->enum_values.push_back("rectilinear");
|
||||
def->enum_values.push_back("concentric");
|
||||
def->enum_values.push_back("spiralinset");
|
||||
def->enum_values.push_back("rectilinear_interlaced");
|
||||
def->enum_values.push_back("grid");
|
||||
def->enum_labels.push_back(L("Default"));
|
||||
def->enum_labels.push_back(L("Rectilinear"));
|
||||
def->enum_labels.push_back(L("Concentric"));
|
||||
def->enum_labels.push_back(L("Spiral Inset"));
|
||||
def->enum_labels.push_back(L("Rectilinear Interlaced"));
|
||||
def->enum_labels.push_back(L("Grid"));
|
||||
def->mode = comAdvanced;
|
||||
|
||||
@@ -113,7 +113,7 @@ enum InfillPattern : int {
|
||||
ipCubic, ipAdaptiveCubic, ipQuarterCubic, ipSupportCubic, ipLightning,
|
||||
ipHoneycomb, ip3DHoneycomb, ipLateralHoneycomb, ipLateralLattice,
|
||||
ipCrossHatch, ipTpmsD, ipTpmsFK, ipGyroid,
|
||||
ipConcentric, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral,
|
||||
ipConcentric, ipSpiralInset, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral,
|
||||
ipSupportBase, ipConcentricInternal,
|
||||
ipCount,
|
||||
};
|
||||
@@ -271,7 +271,7 @@ enum LongRectrationLevel
|
||||
};
|
||||
|
||||
enum SupportMaterialInterfacePattern {
|
||||
smipAuto, smipRectilinear, smipConcentric, smipRectilinearInterlaced, smipGrid
|
||||
smipAuto, smipRectilinear, smipConcentric, smipSpiralInset, smipRectilinearInterlaced, smipGrid
|
||||
};
|
||||
|
||||
// BBS
|
||||
@@ -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(
|
||||
@@ -2148,11 +2153,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
|
||||
|
||||
+192
-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"
|
||||
@@ -1480,13 +1511,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"
|
||||
@@ -1604,7 +1631,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 +3038,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 +3109,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 +3144,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 +3172,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 +3185,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 +3215,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 +3243,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 +3252,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 +3261,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 +3278,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 +3387,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 +3401,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 +3432,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 +3478,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 +3504,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 +3514,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);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <functional>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
|
||||
#include <libslic3r/OpenVDBUtils.hpp>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -65,11 +65,13 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
|
||||
const bool smooth_supports = support_params.support_style != smsGrid;
|
||||
SupportGeneratorLayersPtr &interface_layers = base_and_interface_layers.first;
|
||||
SupportGeneratorLayersPtr &base_interface_layers = base_and_interface_layers.second;
|
||||
// The user-facing interface layer counts include the contact layer. Internally,
|
||||
// contact layers are generated separately, so only the remaining layers are
|
||||
// projected into intermediate interface/base-interface layers here.
|
||||
const size_t num_top_interface_layers = support_params.has_top_contacts ? support_params.num_top_interface_layers - 1 : 0;
|
||||
const size_t num_bottom_interface_layers = support_params.has_bottom_contacts ? support_params.num_bottom_interface_layers - 1 : 0;
|
||||
// Contacts printed separately consume one requested interface layer. Organic
|
||||
// bottom contacts are projection seeds and are not printed separately.
|
||||
const bool organic_tree = support_params.support_style == smsTreeOrganic;
|
||||
const size_t num_top_interface_layers = support_params.has_top_contacts ?
|
||||
support_params.num_top_interface_layers - 1 : 0;
|
||||
const size_t num_bottom_interface_layers = support_params.has_bottom_contacts ?
|
||||
support_params.num_bottom_interface_layers - (organic_tree ? 0 : 1) : 0;
|
||||
const size_t num_top_base_interface_layers = std::min(support_params.num_top_base_interface_layers, num_top_interface_layers);
|
||||
const size_t num_bottom_base_interface_layers = std::min(support_params.num_bottom_base_interface_layers, num_bottom_interface_layers);
|
||||
const size_t num_top_interface_layers_only = num_top_interface_layers - num_top_base_interface_layers;
|
||||
@@ -1652,28 +1654,32 @@ void generate_support_toolpaths(
|
||||
if (top_contact_layer.could_merge(interface_layer) && ! raft_layer)
|
||||
top_contact_layer.merge(std::move(interface_layer));
|
||||
}
|
||||
if (!bottom_interfaces && support_params.can_merge_support_regions) {
|
||||
if (base_layer.could_merge(bottom_contact_layer))
|
||||
base_layer.merge(std::move(bottom_contact_layer));
|
||||
else if (base_layer.empty() && ! bottom_contact_layer.empty() && ! bottom_contact_layer.layer->bridging)
|
||||
base_layer = std::move(bottom_contact_layer);
|
||||
} else if (bottom_contact_layer.could_merge(top_contact_layer) && ! raft_layer) {
|
||||
if (top_interfaces && bottom_interfaces) {
|
||||
top_contact_layer.merge(std::move(bottom_contact_layer));
|
||||
} else if (bottom_interfaces) {
|
||||
top_contact_layer.set_polygons_to_extrude(
|
||||
diff(top_contact_layer.polygons_to_extrude(), bottom_contact_layer.polygons_to_extrude()));
|
||||
} else {
|
||||
bottom_contact_layer.set_polygons_to_extrude(
|
||||
diff(bottom_contact_layer.polygons_to_extrude(), top_contact_layer.polygons_to_extrude()));
|
||||
}
|
||||
} else if (bottom_contact_layer.could_merge(interface_layer) && ! organic_tree) {
|
||||
const bool interface_layer_is_bottom = interface_layer.layer->layer_type == SupporLayerType::BottomInterface;
|
||||
if (bottom_interfaces && interface_layer_is_bottom) {
|
||||
bottom_contact_layer.merge(std::move(interface_layer));
|
||||
} else {
|
||||
bottom_contact_layer.set_polygons_to_extrude(
|
||||
diff(bottom_contact_layer.polygons_to_extrude(), interface_layer.polygons_to_extrude()));
|
||||
// Orca: Organic bottom contacts are projection seeds, not same-layer toolpaths.
|
||||
// Do not merge them into another same-layer support region.
|
||||
if (!organic_tree) {
|
||||
if (!bottom_interfaces && support_params.can_merge_support_regions) {
|
||||
if (base_layer.could_merge(bottom_contact_layer))
|
||||
base_layer.merge(std::move(bottom_contact_layer));
|
||||
else if (base_layer.empty() && ! bottom_contact_layer.empty() && ! bottom_contact_layer.layer->bridging)
|
||||
base_layer = std::move(bottom_contact_layer);
|
||||
} else if (bottom_contact_layer.could_merge(top_contact_layer) && ! raft_layer) {
|
||||
if (top_interfaces && bottom_interfaces) {
|
||||
top_contact_layer.merge(std::move(bottom_contact_layer));
|
||||
} else if (bottom_interfaces) {
|
||||
top_contact_layer.set_polygons_to_extrude(
|
||||
diff(top_contact_layer.polygons_to_extrude(), bottom_contact_layer.polygons_to_extrude()));
|
||||
} else {
|
||||
bottom_contact_layer.set_polygons_to_extrude(
|
||||
diff(bottom_contact_layer.polygons_to_extrude(), top_contact_layer.polygons_to_extrude()));
|
||||
}
|
||||
} else if (bottom_contact_layer.could_merge(interface_layer)) {
|
||||
const bool interface_layer_is_bottom = interface_layer.layer->layer_type == SupporLayerType::BottomInterface;
|
||||
if (bottom_interfaces && interface_layer_is_bottom) {
|
||||
bottom_contact_layer.merge(std::move(interface_layer));
|
||||
} else {
|
||||
bottom_contact_layer.set_polygons_to_extrude(
|
||||
diff(bottom_contact_layer.polygons_to_extrude(), interface_layer.polygons_to_extrude()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -141,6 +141,8 @@ struct SupportParameters {
|
||||
this->contact_fill_pattern = ipGrid;
|
||||
else if (object_config.support_interface_pattern == smipRectilinearInterlaced)
|
||||
this->contact_fill_pattern = ipRectilinear;
|
||||
else if (object_config.support_interface_pattern == smipSpiralInset)
|
||||
this->contact_fill_pattern = ipSpiralInset;
|
||||
else
|
||||
this->contact_fill_pattern =
|
||||
(object_config.support_interface_pattern == smipAuto && zero_gap_contact_interface) ||
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Slic3r::TreeSupport3D
|
||||
using namespace std::literals;
|
||||
|
||||
// or warning
|
||||
// had to use a define beacuse the macro processing inside macro BOOST_LOG_TRIVIAL()
|
||||
// had to use a define because the macro processing inside macro BOOST_LOG_TRIVIAL()
|
||||
#define error_level_not_in_cache debug
|
||||
|
||||
//FIXME Machine border is currently ignored.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <deque>
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
@@ -607,6 +608,17 @@ static inline std::vector<IntersectionLines> slice_make_lines(
|
||||
}
|
||||
}
|
||||
);
|
||||
// Facet processing above is parallel, so per-layer line order depends on thread scheduling,
|
||||
// and make_loops() derives island order and loop start vertices from it. Sort canonically;
|
||||
// edge_type and flags only break ties, std::sort being unstable.
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, lines.size()),
|
||||
[&lines](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t i = range.begin(); i < range.end(); ++ i)
|
||||
std::sort(lines[i].begin(), lines[i].end(), [](const IntersectionLine &l, const IntersectionLine &r) {
|
||||
return std::make_tuple(l.edge_a_id, l.edge_b_id, l.a_id, l.b_id, l.a.x(), l.a.y(), l.b.x(), l.b.y(), l.edge_type, l.flags) <
|
||||
std::make_tuple(r.edge_a_id, r.edge_b_id, r.a_id, r.b_id, r.a.x(), r.a.y(), r.b.x(), r.b.y(), r.edge_type, r.flags);
|
||||
});
|
||||
});
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
||||
@@ -255,6 +255,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"); }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
#define SLIC3R_APP_KEY "@SLIC3R_APP_KEY@"
|
||||
#define SLIC3R_VERSION "@SLIC3R_VERSION@"
|
||||
#define SoftFever_VERSION "@SoftFever_VERSION@"
|
||||
#ifndef GIT_COMMIT_HASH
|
||||
#define GIT_COMMIT_HASH "0000000" // 0000000 means uninitialized
|
||||
#endif
|
||||
#define SLIC3R_BUILD_ID "@SLIC3R_BUILD_ID@"
|
||||
//#define SLIC3R_RC_VERSION "@SLIC3R_VERSION@"
|
||||
#define BBL_INTERNAL_TESTING @BBL_INTERNAL_TESTING@
|
||||
|
||||
+25
-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");
|
||||
|
||||
@@ -63,6 +63,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/BitmapComboBox.hpp
|
||||
GUI/BonjourDialog.cpp
|
||||
GUI/BonjourDialog.hpp
|
||||
GUI/BuildCommit.cpp
|
||||
GUI/BuildCommit.hpp
|
||||
GUI/CrealityDiscoveryDialog.cpp
|
||||
GUI/CrealityDiscoveryDialog.hpp
|
||||
GUI/calib_dlg.cpp
|
||||
@@ -95,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
|
||||
@@ -452,6 +456,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
|
||||
@@ -855,6 +861,18 @@ source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SLIC3R_GUI_SOURCES})
|
||||
|
||||
encoding_check(libslic3r_gui)
|
||||
|
||||
# Only BuildCommit.cpp includes the generated header, plus BaseException.cpp on
|
||||
# Windows. Both build into libslic3r_gui, so the header only has to exist before
|
||||
# that target builds.
|
||||
set(_git_commit_hash_header "${CMAKE_CURRENT_BINARY_DIR}/git_commit_hash.h")
|
||||
add_custom_target(git_commit_hash_header
|
||||
BYPRODUCTS "${_git_commit_hash_header}"
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
"-DSOURCE_DIR=${CMAKE_SOURCE_DIR}"
|
||||
"-DOUT_FILE=${_git_commit_hash_header}"
|
||||
-P "${CMAKE_CURRENT_LIST_DIR}/GitCommitHash.cmake"
|
||||
COMMENT "Resolving the git commit hash")
|
||||
add_dependencies(libslic3r_gui git_commit_hash_header)
|
||||
|
||||
if(APPLE AND CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
|
||||
set(_opengl_link_lib "")
|
||||
@@ -912,6 +930,9 @@ endif ()
|
||||
|
||||
if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY)
|
||||
add_precompiled_header(libslic3r_gui pchheader.hpp FORCEINCLUDE)
|
||||
elseif (MSVC)
|
||||
# Puts the Windows headers first when the PCH is off.
|
||||
target_compile_options(libslic3r_gui PRIVATE "/FIslic3r/win_platform.hpp")
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
@@ -980,10 +1001,6 @@ endif ()
|
||||
# Add a definition so that we can tell we are compiling slic3r.
|
||||
target_compile_definitions(libslic3r_gui PRIVATE SLIC3R_CURRENTLY_COMPILING_GUI_MODULE)
|
||||
|
||||
if(ORCA_BUNDLED_UV_EXECUTABLE_CONFIG)
|
||||
target_compile_definitions(libslic3r_gui PRIVATE "ORCA_BUNDLED_UV_EXECUTABLE=\"${ORCA_BUNDLED_UV_EXECUTABLE_CONFIG}\"")
|
||||
endif()
|
||||
|
||||
if (ORCA_BUILD_PYTHON_STUBGEN_MODULE)
|
||||
add_library(orca_stubgen MODULE
|
||||
plugin/PythonPluginBridge.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"
|
||||
@@ -1511,6 +1512,10 @@ void AMSDryCtrWin::update_filament_guide_info(DevAms* dev_ams)
|
||||
m_temperature_input->GetValue().ToLong(&input_temp);
|
||||
bool can_start = true;
|
||||
|
||||
// "GFA00" is Bambu's PLA id; GetFilamentDryingPreset is keyed by our OF ids.
|
||||
auto* agent = wxGetApp().getAgent();
|
||||
const std::string pla_filament_id = agent ? agent->to_orca_filament_id("GFA00") : std::string("GFA00");
|
||||
|
||||
int slot_count = 0, empty_count = 0;
|
||||
for (auto& tray_pair : dev_ams->GetTrays()) {
|
||||
if (!tray_pair.second) {
|
||||
@@ -1526,13 +1531,15 @@ void AMSDryCtrWin::update_filament_guide_info(DevAms* dev_ams)
|
||||
wxString filament_type = tray_pair.second->get_display_filament_type();
|
||||
DevFilamentDryingPreset preset;
|
||||
if (filament_type.IsEmpty()) {
|
||||
auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00");
|
||||
auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset(pla_filament_id);
|
||||
if (!fallback_preset) continue; // no PLA preset (e.g. the id map is missing): skip, don't throw
|
||||
preset = fallback_preset.value();
|
||||
filament_type = "?";
|
||||
} else if (preset_opt.has_value()) {
|
||||
preset = preset_opt.value();
|
||||
} else {
|
||||
auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00");
|
||||
auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset(pla_filament_id);
|
||||
if (!fallback_preset) continue;
|
||||
preset = fallback_preset.value();
|
||||
}
|
||||
std::string icon_path = "dev_ams_dry_ctr_enable";
|
||||
@@ -1594,39 +1601,21 @@ int AMSDryCtrWin::update_filament_list(DevAms* dev_ams, MachineObject* obj)
|
||||
}
|
||||
stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(extruder_id);
|
||||
std::string nozzle_diameter_str = stream.str();
|
||||
std::set<std::string> printer_names = preset_bundle->get_printer_names_by_printer_type_and_nozzle(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str);
|
||||
|
||||
for (auto filament_it = filaments.begin(); filament_it != filaments.end(); ++filament_it) {
|
||||
Preset& preset = *filament_it;
|
||||
// Filter by system preset: root preset and (system preset or user preset is supported)
|
||||
if (filaments.get_preset_base(*filament_it) != &preset || (!filament_it->is_system && !obj->is_support_user_preset)) {
|
||||
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
if (!filament_id_set.insert(filament_it->filament_id).second)
|
||||
continue;
|
||||
const std::string filament_alias = filaments.get_preset_alias(*filament_it, true);
|
||||
if (filament_alias.empty())
|
||||
continue;
|
||||
auto opt_info = preset_bundle->get_filament_by_filament_id(filament_it->filament_id);
|
||||
if (!opt_info.has_value())
|
||||
continue;
|
||||
}
|
||||
|
||||
ConfigOption * printer_opt = filament_it->config.option("compatible_printers");
|
||||
ConfigOptionStrings *printer_strs = dynamic_cast<ConfigOptionStrings *>(printer_opt);
|
||||
if (!printer_strs) continue;
|
||||
|
||||
for (auto printer_str : printer_strs->values) {
|
||||
if (printer_names.find(printer_str) != printer_names.end()) {
|
||||
if (filament_id_set.find(filament_it->filament_id) != filament_id_set.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filament_id_set.insert(filament_it->filament_id);
|
||||
auto filament_alias = filaments.get_preset_alias(*filament_it, true);
|
||||
if (!filament_alias.empty()) {
|
||||
auto opt_info = preset_bundle->get_filament_by_filament_id(filament_it->filament_id);
|
||||
if (opt_info.has_value()) {
|
||||
auto real_info = opt_info.value();
|
||||
real_info.filament_name = filament_alias;
|
||||
m_tray_ids.push_back(std::move(real_info));
|
||||
m_trays_combo->Append(wxString::FromUTF8(filament_alias));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
opt_info->filament_name = filament_alias;
|
||||
m_tray_ids.push_back(std::move(*opt_info));
|
||||
m_trays_combo->Append(wxString::FromUTF8(filament_alias));
|
||||
}
|
||||
|
||||
if (m_tray_ids.empty()) {
|
||||
@@ -1701,9 +1690,10 @@ int AMSDryCtrWin::update_filament_list(DevAms* dev_ams, MachineObject* obj)
|
||||
|
||||
// Select recommended drying temperature and default filament
|
||||
float min_dry_temp = std::numeric_limits<float>::max();
|
||||
std::string default_filament_id = "GFA00";
|
||||
auto* agent = wxGetApp().getAgent();
|
||||
std::string default_filament_id = agent ? agent->to_orca_filament_id("GFA00") : std::string("GFA00"); // compared against m_tray_ids[i].filament_id (our OF ids) below
|
||||
bool has_ready = false;
|
||||
const auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00");
|
||||
const auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset(default_filament_id);
|
||||
for (const auto& tray_pair : dev_ams->GetTrays()) {
|
||||
if (!tray_pair.second || !tray_pair.second->is_tray_info_ready()) continue;
|
||||
has_ready = true;
|
||||
|
||||
@@ -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>
|
||||
@@ -681,6 +683,19 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
}
|
||||
|
||||
|
||||
// Orca: log the tray payload this dialog hands the printer, so the filament_id resolved from the
|
||||
// dropdown selection can be checked against the tray_info_idx the AMS actually receives. A
|
||||
// BBL-tagged (RFID) tray is read-only here, so nothing is published for it.
|
||||
BOOST_LOG_TRIVIAL(info) << "ams_materials_setting: " << (m_is_third ? "sending" : "NOT sending (BBL RFID tray, read-only)")
|
||||
<< ", ams_id = " << ams_id << ", slot_id = " << slot_id
|
||||
<< ", selected = " << m_comboBox_filament->GetValue().ToStdString()
|
||||
<< ", tray_info_idx (filament_id) = " << ams_filament_id
|
||||
<< ", setting_id = " << ams_setting_id
|
||||
<< ", tray_type = " << m_filament_type
|
||||
<< ", tray_color = " << col_buf
|
||||
<< ", nozzle_temp_min = " << nozzle_temp_min_int
|
||||
<< ", nozzle_temp_max = " << nozzle_temp_max_int;
|
||||
|
||||
// set filament
|
||||
if (m_is_third) {
|
||||
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, std::string(col_buf), m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int);
|
||||
@@ -802,7 +817,10 @@ void AMSMaterialsSetting::set_color(wxColour color)
|
||||
fila_color.m_colors.insert(color);
|
||||
fila_color.EndSet(m_clr_picker->ctype);
|
||||
auto clr_query = GUI::wxGetApp().get_filament_color_code_query();
|
||||
m_clr_name->SetLabelText(clr_query->GetFilaColorName(ams_filament_id, fila_color));
|
||||
// ams_filament_id is our OF id; GetFilaColorName looks up filaments_color_codes.json,
|
||||
// downloaded from Bambu and keyed by the printer's own ids, so translate for this lookup only.
|
||||
auto* agent = GUI::wxGetApp().getAgent();
|
||||
m_clr_name->SetLabelText(clr_query->GetFilaColorName(agent ? agent->from_orca_filament_id(ams_filament_id) : ams_filament_id, fila_color));
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::set_empty_color(wxColour color)
|
||||
@@ -823,7 +841,10 @@ void AMSMaterialsSetting::set_colors(std::vector<wxColour> colors)
|
||||
for (const auto& clr : colors) { fila_color.m_colors.insert(clr); }
|
||||
fila_color.EndSet(m_clr_picker->ctype);
|
||||
auto clr_query = GUI::wxGetApp().get_filament_color_code_query();
|
||||
m_clr_name->SetLabelText(clr_query->GetFilaColorName(ams_filament_id, fila_color));
|
||||
// ams_filament_id is our OF id; GetFilaColorName looks up filaments_color_codes.json,
|
||||
// downloaded from Bambu and keyed by the printer's own ids, so translate for this lookup only.
|
||||
auto* agent = GUI::wxGetApp().getAgent();
|
||||
m_clr_name->SetLabelText(clr_query->GetFilaColorName(agent ? agent->from_orca_filament_id(ams_filament_id) : ams_filament_id, fila_color));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,7 +953,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
m_input_k_val->GetTextCtrl()->SetValue(k);
|
||||
m_input_n_val->GetTextCtrl()->SetValue(n);
|
||||
|
||||
int idx = 0;
|
||||
wxArrayString filament_items;
|
||||
wxString bambu_filament_name;
|
||||
wxString hint_filament_name; // the hint type to be selected
|
||||
@@ -940,6 +960,9 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
std::unordered_map<wxString, wxString> query_filament_types; //
|
||||
|
||||
std::set<std::string> filament_id_set;
|
||||
// The alias keyed map has to start empty: it is a member, so a stale alias left by an earlier
|
||||
// popup (a different printer, a different nozzle) would resolve to that printer's filament_id.
|
||||
map_filament_items.clear();
|
||||
PresetBundle * preset_bundle = wxGetApp().preset_bundle;
|
||||
std::ostringstream stream;
|
||||
// Defensive: this dialog is opened only from StatusPanel (BBL-only) today, so the fallback fires
|
||||
@@ -952,83 +975,48 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
}
|
||||
stream << std::fixed << std::setprecision(1) << machine_diameter;
|
||||
std::string nozzle_diameter_str = stream.str();
|
||||
std::set<std::string> printer_names = preset_bundle->get_printer_names_by_printer_type_and_nozzle(DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str);
|
||||
|
||||
if (preset_bundle) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "system_preset_bundle filament number=" << preset_bundle->filaments.size();
|
||||
for (auto filament_it = preset_bundle->filaments.begin(); filament_it != preset_bundle->filaments.end(); filament_it++) {
|
||||
//filter by system preset
|
||||
Preset& preset = *filament_it;
|
||||
/*The situation where the user preset is not displayed is as follows:
|
||||
1. Not a root preset
|
||||
2. Not system preset and the printer firmware does not support user preset */
|
||||
if (preset_bundle->filaments.get_preset_base(*filament_it) != &preset || (!filament_it->is_system && !obj->is_support_user_preset)) {
|
||||
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
if (!filament_id_set.insert(filament_it->filament_id).second)
|
||||
continue;
|
||||
const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true);
|
||||
if (alias.empty())
|
||||
continue;
|
||||
}
|
||||
|
||||
ConfigOption * printer_opt = filament_it->config.option("compatible_printers");
|
||||
ConfigOptionStrings *printer_strs = dynamic_cast<ConfigOptionStrings *>(printer_opt);
|
||||
for (auto printer_str : printer_strs->values) {
|
||||
if (printer_names.find(printer_str) != printer_names.end()) {
|
||||
if (filament_id_set.find(filament_it->filament_id) != filament_id_set.end()) {
|
||||
continue;
|
||||
} else {
|
||||
filament_id_set.insert(filament_it->filament_id);
|
||||
// name matched
|
||||
if (filament_it->is_system) {
|
||||
filament_items.push_back(filament_it->alias);
|
||||
_collect_filament_info(filament_it->alias, preset, query_filament_vendors, query_filament_types);
|
||||
filament_items.push_back(alias);
|
||||
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
|
||||
|
||||
FilamentInfos filament_infos;
|
||||
filament_infos.filament_id = filament_it->filament_id;
|
||||
filament_infos.setting_id = filament_it->setting_id;
|
||||
map_filament_items[filament_it->alias] = filament_infos;
|
||||
} else {
|
||||
char target = '@';
|
||||
size_t pos = filament_it->name.find(target);
|
||||
if (pos != std::string::npos) {
|
||||
std::string user_preset_alias = filament_it->name.substr(0, pos - 1);
|
||||
wxString wx_user_preset_alias = wxString(user_preset_alias.c_str(), wxConvUTF8);
|
||||
user_preset_alias = wx_user_preset_alias.ToStdString();
|
||||
FilamentInfos filament_infos;
|
||||
filament_infos.filament_id = filament_it->filament_id;
|
||||
filament_infos.setting_id = filament_it->setting_id;
|
||||
map_filament_items[alias] = filament_infos;
|
||||
|
||||
filament_items.push_back(user_preset_alias);
|
||||
_collect_filament_info(user_preset_alias, preset, query_filament_vendors, query_filament_types);
|
||||
|
||||
FilamentInfos filament_infos;
|
||||
filament_infos.filament_id = filament_it->filament_id;
|
||||
filament_infos.setting_id = filament_it->setting_id;
|
||||
map_filament_items[user_preset_alias] = filament_infos;
|
||||
}
|
||||
}
|
||||
|
||||
if (filament_it->filament_id == ams_filament_id) {
|
||||
hint_filament_name = from_u8(filament_it->alias);
|
||||
bambu_filament_name = from_u8(filament_it->alias);
|
||||
if (filament_it->filament_id == ams_filament_id) {
|
||||
hint_filament_name = from_u8(alias);
|
||||
bambu_filament_name = from_u8(alias);
|
||||
|
||||
|
||||
// update if nozzle_temperature_range is found
|
||||
ConfigOption *opt_min = filament_it->config.option("nozzle_temperature_range_low");
|
||||
if (opt_min) {
|
||||
ConfigOptionInts *opt_min_ints = dynamic_cast<ConfigOptionInts *>(opt_min);
|
||||
if (opt_min_ints) {
|
||||
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(0));
|
||||
m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min);
|
||||
}
|
||||
}
|
||||
ConfigOption *opt_max = filament_it->config.option("nozzle_temperature_range_high");
|
||||
if (opt_max) {
|
||||
ConfigOptionInts *opt_max_ints = dynamic_cast<ConfigOptionInts *>(opt_max);
|
||||
if (opt_max_ints) {
|
||||
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(0));
|
||||
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max);
|
||||
}
|
||||
}
|
||||
}
|
||||
idx++;
|
||||
// update if nozzle_temperature_range is found
|
||||
ConfigOption *opt_min = filament_it->config.option("nozzle_temperature_range_low");
|
||||
if (opt_min) {
|
||||
ConfigOptionInts *opt_min_ints = dynamic_cast<ConfigOptionInts *>(opt_min);
|
||||
if (opt_min_ints) {
|
||||
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(0));
|
||||
m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min);
|
||||
}
|
||||
}
|
||||
ConfigOption *opt_max = filament_it->config.option("nozzle_temperature_range_high");
|
||||
if (opt_max) {
|
||||
ConfigOptionInts *opt_max_ints = dynamic_cast<ConfigOptionInts *>(opt_max);
|
||||
if (opt_max_ints) {
|
||||
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(0));
|
||||
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1251,56 +1239,47 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
stream << std::fixed << std::setprecision(1) << machine_diameter;
|
||||
}
|
||||
std::string nozzle_diameter_str = stream.str();
|
||||
std::set<std::string> printer_names = preset_bundle->get_printer_names_by_printer_type_and_nozzle(DevPrinterConfigUtil::get_printer_display_name(obj->printer_type),
|
||||
nozzle_diameter_str);
|
||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||
if (!m_comboBox_filament->GetValue().IsEmpty()) {
|
||||
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
||||
std::string filament_id = filament_item.filament_id;
|
||||
if (it->filament_id.compare(filament_id) == 0) {
|
||||
ConfigOption * printer_opt = it->config.option("compatible_printers");
|
||||
ConfigOptionStrings *printer_strs = dynamic_cast<ConfigOptionStrings *>(printer_opt);
|
||||
bool has_compatible_printer = false;
|
||||
for (auto printer_str : printer_strs->values) {
|
||||
if (printer_names.find(printer_str) != printer_names.end()) {
|
||||
has_compatible_printer = true;
|
||||
break;
|
||||
}
|
||||
// Resolve the selection against the same list Popup() built the dropdown from, so the two
|
||||
// halves of the dialog cannot disagree about which filaments this machine can use.
|
||||
const std::string selected = m_comboBox_filament->GetValue().ToStdString();
|
||||
if (!selected.empty()) {
|
||||
const std::string filament_id = map_filament_items[selected].filament_id;
|
||||
for (Preset *it : preset_bundle->get_filament_presets_for_machine(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
if (it->filament_id != filament_id)
|
||||
continue;
|
||||
// ) if nozzle_temperature_range is found
|
||||
ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low");
|
||||
if (opt_min) {
|
||||
ConfigOptionInts* opt_min_ints = dynamic_cast<ConfigOptionInts*>(opt_min);
|
||||
if (opt_min_ints) {
|
||||
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(0));
|
||||
m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min);
|
||||
}
|
||||
if (!it->is_system && !has_compatible_printer) continue;
|
||||
// ) if nozzle_temperature_range is found
|
||||
ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low");
|
||||
if (opt_min) {
|
||||
ConfigOptionInts* opt_min_ints = dynamic_cast<ConfigOptionInts*>(opt_min);
|
||||
if (opt_min_ints) {
|
||||
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(0));
|
||||
m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min);
|
||||
}
|
||||
}
|
||||
ConfigOption* opt_max = it->config.option("nozzle_temperature_range_high");
|
||||
if (opt_max) {
|
||||
ConfigOptionInts* opt_max_ints = dynamic_cast<ConfigOptionInts*>(opt_max);
|
||||
if (opt_max_ints) {
|
||||
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(0));
|
||||
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max);
|
||||
}
|
||||
}
|
||||
ConfigOption* opt_type = it->config.option("filament_type");
|
||||
bool found_filament_type = false;
|
||||
if (opt_type) {
|
||||
ConfigOptionStrings* opt_type_strs = dynamic_cast<ConfigOptionStrings*>(opt_type);
|
||||
if (opt_type_strs) {
|
||||
found_filament_type = true;
|
||||
//m_filament_type = opt_type_strs->get_at(0);
|
||||
std::string display_filament_type;
|
||||
m_filament_type = it->config.get_filament_type(display_filament_type);
|
||||
}
|
||||
}
|
||||
if (!found_filament_type)
|
||||
m_filament_type = "";
|
||||
|
||||
break;
|
||||
}
|
||||
ConfigOption* opt_max = it->config.option("nozzle_temperature_range_high");
|
||||
if (opt_max) {
|
||||
ConfigOptionInts* opt_max_ints = dynamic_cast<ConfigOptionInts*>(opt_max);
|
||||
if (opt_max_ints) {
|
||||
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(0));
|
||||
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max);
|
||||
}
|
||||
}
|
||||
ConfigOption* opt_type = it->config.option("filament_type");
|
||||
bool found_filament_type = false;
|
||||
if (opt_type) {
|
||||
ConfigOptionStrings* opt_type_strs = dynamic_cast<ConfigOptionStrings*>(opt_type);
|
||||
if (opt_type_strs) {
|
||||
found_filament_type = true;
|
||||
//m_filament_type = opt_type_strs->get_at(0);
|
||||
std::string display_filament_type;
|
||||
m_filament_type = it->config.get_filament_type(display_filament_type);
|
||||
}
|
||||
}
|
||||
if (!found_filament_type)
|
||||
m_filament_type = "";
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1938,11 +1917,6 @@ void ColorPickerPopup::paintEvent(wxPaintEvent& evt)
|
||||
|
||||
void ColorPickerPopup::OnDismiss() {}
|
||||
|
||||
void ColorPickerPopup::Popup()
|
||||
{
|
||||
PopupWindow::Popup();
|
||||
}
|
||||
|
||||
bool ColorPickerPopup::ProcessLeftDown(wxMouseEvent& event) {
|
||||
return PopupWindow::ProcessLeftDown(event);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,6 @@ public:
|
||||
void set_ams_colours(std::vector<wxColour> ams);
|
||||
void set_def_colour(wxColour col);
|
||||
void paintEvent(wxPaintEvent& evt);
|
||||
void Popup();
|
||||
virtual void OnDismiss() wxOVERRIDE;
|
||||
virtual bool ProcessLeftDown(wxMouseEvent& event) wxOVERRIDE;
|
||||
|
||||
|
||||
@@ -292,7 +292,7 @@ void AMSSetting::UpdateByObj(MachineObject* obj)
|
||||
|
||||
update_ams_img(obj);
|
||||
|
||||
m_ams_type->Update(obj);
|
||||
m_ams_type->UpdateInfo(obj);
|
||||
//m_ams_arrange_order->Update(obj);
|
||||
update_insert_material_read_mode(obj);
|
||||
m_sizer_remain_block->Show(obj->is_support_update_remain);
|
||||
@@ -624,7 +624,7 @@ void AMSSettingTypePanel::CreateGui()
|
||||
Fit();
|
||||
}
|
||||
|
||||
void AMSSettingTypePanel::Update(const MachineObject* obj)
|
||||
void AMSSettingTypePanel::UpdateInfo(const MachineObject* obj)
|
||||
{
|
||||
if (!obj) {
|
||||
Show(false);
|
||||
|
||||
@@ -110,7 +110,7 @@ public:
|
||||
~AMSSettingTypePanel();
|
||||
|
||||
public:
|
||||
void Update(const MachineObject* obj);
|
||||
void UpdateInfo(const MachineObject* obj);
|
||||
|
||||
private:
|
||||
void CreateGui();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "BuildCommit.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
@@ -245,7 +246,7 @@ AboutDialog::AboutDialog()
|
||||
vesizer->Add(0, 0, 1, wxEXPAND, FromDIP(5));
|
||||
auto version_string = std::string(SoftFever_VERSION); // _L("Orca Slicer ") + " " + std::string(SoftFever_VERSION);
|
||||
wxStaticText* version = new wxStaticText(this, wxID_ANY, version_string.c_str(), wxDefaultPosition, wxDefaultSize);
|
||||
wxStaticText* credits_string = new wxStaticText(this, wxID_ANY, wxString::Format("Build %s", std::string(GIT_COMMIT_HASH)), wxDefaultPosition, wxDefaultSize);
|
||||
wxStaticText* credits_string = new wxStaticText(this, wxID_ANY, wxString::Format("Build %s", build_commit_label), wxDefaultPosition, wxDefaultSize);
|
||||
credits_string->SetFont(_build_string_font);
|
||||
wxFont version_font = GetFont();
|
||||
version_font = version_font.Scaled(1.85f); // SetPointSize(20) not works on macOS because it uses a 72 PPI reference
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#include "BuildCommit.hpp"
|
||||
#include "git_commit_hash.h"
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
const char *const build_commit_hash = GIT_COMMIT_HASH;
|
||||
const char *const build_commit_label = GIT_COMMIT_HASH GIT_COMMIT_SUFFIX;
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
// Read these rather than including git_commit_hash.h, which changes with every
|
||||
// commit and rebuilds everything that includes it.
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// The commit alone, safe to use in a commit URL.
|
||||
extern const char *const build_commit_hash;
|
||||
|
||||
// The same, with "-dirty" when the build had uncommitted changes. Use this
|
||||
// wherever the build is shown to a person.
|
||||
extern const char *const build_commit_label;
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -702,7 +702,6 @@ wxArrayString NewCalibrationHistoryDialog::get_all_filaments(const MachineObject
|
||||
|
||||
wxArrayString filament_items;
|
||||
std::set<std::string> filament_id_set;
|
||||
std::set<std::string> printer_names;
|
||||
std::ostringstream stream;
|
||||
// If the machine didn't report a nozzle diameter (0.0 = unknown), fall back to the currently
|
||||
// selected printer preset so the filament list isn't empty.
|
||||
@@ -714,67 +713,21 @@ wxArrayString NewCalibrationHistoryDialog::get_all_filaments(const MachineObject
|
||||
stream << std::fixed << std::setprecision(1) << machine_diameter;
|
||||
std::string nozzle_diameter_str = stream.str();
|
||||
|
||||
for (auto printer_it = preset_bundle->printers.begin(); printer_it != preset_bundle->printers.end(); printer_it++) {
|
||||
// filter by system preset
|
||||
if (!printer_it->is_system)
|
||||
continue;
|
||||
// get printer_model
|
||||
ConfigOption * printer_model_opt = printer_it->config.option("printer_model");
|
||||
ConfigOptionString *printer_model_str = dynamic_cast<ConfigOptionString *>(printer_model_opt);
|
||||
if (!printer_model_str)
|
||||
continue;
|
||||
|
||||
// use printer_model as printer type
|
||||
if (printer_model_str->value != DevPrinterConfigUtil::get_printer_display_name(obj->printer_type))
|
||||
continue;
|
||||
|
||||
if (printer_it->name.find(nozzle_diameter_str) != std::string::npos)
|
||||
printer_names.insert(printer_it->name);
|
||||
}
|
||||
|
||||
if (preset_bundle) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "system_preset_bundle filament number=" << preset_bundle->filaments.size();
|
||||
for (auto filament_it = preset_bundle->filaments.begin(); filament_it != preset_bundle->filaments.end(); filament_it++) {
|
||||
// filter by system preset
|
||||
Preset &preset = *filament_it;
|
||||
/*The situation where the user preset is not displayed is as follows:
|
||||
1. Not a root preset
|
||||
2. Not system preset and the printer firmware does not support user preset */
|
||||
if (preset_bundle->filaments.get_preset_base(*filament_it) != &preset || (!filament_it->is_system && ! obj->is_support_user_preset)) { continue; }
|
||||
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
if (!filament_id_set.insert(filament_it->filament_id).second)
|
||||
continue;
|
||||
const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true);
|
||||
if (alias.empty())
|
||||
continue;
|
||||
|
||||
ConfigOption * printer_opt = filament_it->config.option("compatible_printers");
|
||||
ConfigOptionStrings *printer_strs = dynamic_cast<ConfigOptionStrings *>(printer_opt);
|
||||
for (auto printer_str : printer_strs->values) {
|
||||
if (printer_names.find(printer_str) != printer_names.end()) {
|
||||
if (filament_id_set.find(filament_it->filament_id) != filament_id_set.end()) {
|
||||
continue;
|
||||
} else {
|
||||
filament_id_set.insert(filament_it->filament_id);
|
||||
// name matched
|
||||
if (filament_it->is_system) {
|
||||
filament_items.push_back(filament_it->alias);
|
||||
FilamentInfos filament_infos;
|
||||
filament_infos.filament_id = filament_it->filament_id;
|
||||
filament_infos.setting_id = filament_it->setting_id;
|
||||
map_filament_items[filament_it->alias] = filament_infos;
|
||||
} else {
|
||||
char target = '@';
|
||||
size_t pos = filament_it->name.find(target);
|
||||
if (pos != std::string::npos) {
|
||||
std::string user_preset_alias = filament_it->name.substr(0, pos - 1);
|
||||
wxString wx_user_preset_alias = wxString(user_preset_alias.c_str(), wxConvUTF8);
|
||||
user_preset_alias = wx_user_preset_alias.ToStdString();
|
||||
|
||||
filament_items.push_back(user_preset_alias);
|
||||
FilamentInfos filament_infos;
|
||||
filament_infos.filament_id = filament_it->filament_id;
|
||||
filament_infos.setting_id = filament_it->setting_id;
|
||||
map_filament_items[user_preset_alias] = filament_infos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
filament_items.push_back(alias);
|
||||
FilamentInfos filament_infos;
|
||||
filament_infos.filament_id = filament_it->filament_id;
|
||||
filament_infos.setting_id = filament_it->setting_id;
|
||||
map_filament_items[alias] = filament_infos;
|
||||
}
|
||||
}
|
||||
return filament_items;
|
||||
|
||||
@@ -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"
|
||||
@@ -361,7 +363,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"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user