mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-09 08:41:14 +00:00
Merge branch 'main' into feat/printer-agent-infra
This commit is contained in:
+93
-11
@@ -2116,9 +2116,14 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
// One resolver for the whole run, so presets from the same vendor tree share its load.
|
||||
std::unique_ptr<PresetBundle> system_preset_resolver;
|
||||
auto resolve_preset = [&ensure_cli_preset_bundle, &system_preset_resolver](const std::string &file, DynamicPrintConfig &config,
|
||||
std::string &config_type, const std::string &config_from,
|
||||
bool probe_type, std::string &error) {
|
||||
auto ensure_system_preset_resolver = [&system_preset_resolver]() -> PresetBundle & {
|
||||
if (!system_preset_resolver)
|
||||
system_preset_resolver = std::make_unique<PresetBundle>();
|
||||
return *system_preset_resolver;
|
||||
};
|
||||
auto resolve_preset = [&ensure_cli_preset_bundle, &ensure_system_preset_resolver](const std::string &file, DynamicPrintConfig &config,
|
||||
std::string &config_type, const std::string &config_from,
|
||||
bool probe_type, std::string &error) {
|
||||
const auto *inherits = config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS);
|
||||
if (!probe_type && (inherits == nullptr || inherits->value.empty()))
|
||||
return true;
|
||||
@@ -2126,9 +2131,7 @@ int CLI::run(int argc, char **argv)
|
||||
PresetBundle *bundle = nullptr;
|
||||
bool allow_source_manifest = false;
|
||||
if (config_from == "system") {
|
||||
if (!system_preset_resolver)
|
||||
system_preset_resolver = std::make_unique<PresetBundle>();
|
||||
bundle = system_preset_resolver.get();
|
||||
bundle = &ensure_system_preset_resolver();
|
||||
allow_source_manifest = true;
|
||||
} else {
|
||||
bundle = ensure_cli_preset_bundle(error);
|
||||
@@ -3120,6 +3123,44 @@ int CLI::run(int argc, char **argv)
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Load the project's printer and process settings as the GUI loads its presets: over the default preset,
|
||||
// with every key the project does not list as changed, including keys saved before an option existed,
|
||||
// taken from its current system preset.
|
||||
auto load_project_preset = [this, &ensure_system_preset_resolver, ¤t_different_settings, filament_count](const std::string &system_name, Preset::Type type) {
|
||||
if (system_name.empty())
|
||||
return;
|
||||
// Preset bookkeeping the CLI keeps in its own groups, e.g. inherits_group and print_compatible_printers.
|
||||
static const std::set<std::string> bookkeeping_keys = {"inherits", "compatible_printers", "compatible_prints", "compatible_printers_condition",
|
||||
"compatible_prints_condition", "print_settings_id", "printer_settings_id"};
|
||||
const size_t index = type == Preset::TYPE_PRINTER ? filament_count + 1 : 0;
|
||||
PresetBundle &resolver = ensure_system_preset_resolver();
|
||||
DynamicPrintConfig system_config;
|
||||
t_config_option_keys keys;
|
||||
const DynamicPrintConfig config = Preset::load_external_config(type,
|
||||
type == Preset::TYPE_PRINTER ? resolver.printers.default_preset_for(m_print_config).config : resolver.prints.default_preset().config,
|
||||
m_print_config, PresetBundle::project_different_keys(index < current_different_settings.size() ? current_different_settings[index] : std::string()),
|
||||
[&](const std::string &) -> DynamicPrintConfig * {
|
||||
std::string error;
|
||||
if (resolver.resolve_system_preset(system_config, type, system_name, config_substitution_rule, error))
|
||||
return &system_config;
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("CLI: system preset '%1%' not resolved (%2%); the project keeps its values") % system_name % error;
|
||||
return nullptr;
|
||||
}, &keys);
|
||||
for (const std::string &key : keys) {
|
||||
const ConfigOption *opt = config.option(key);
|
||||
const ConfigOption *old = m_print_config.option(key);
|
||||
if (bookkeeping_keys.count(key) != 0 || opt == nullptr || (old != nullptr && *old == *opt))
|
||||
continue;
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("CLI: %1% from '%2%': %3% -> %4%") % key % system_name % (old ? old->serialize() : std::string("(missing)")) % opt->serialize();
|
||||
m_print_config.set_key_value(key, opt->clone());
|
||||
}
|
||||
};
|
||||
// The --uptodate path refreshes the project from its own system configs.
|
||||
if (new_printer_name.empty() && load_machine_config.empty())
|
||||
load_project_preset(current_printer_system_name, Preset::TYPE_PRINTER);
|
||||
if (new_process_name.empty() && load_process_config.empty())
|
||||
load_project_preset(current_process_system_name, Preset::TYPE_PRINT);
|
||||
|
||||
std::vector<std::string>& different_settings = m_print_config.option<ConfigOptionStrings>("different_settings_to_system", true)->values;
|
||||
std::vector<std::string>& inherits_group = m_print_config.option<ConfigOptionStrings>("inherits_group", true)->values;
|
||||
inherits_group.resize(filament_count + 2, std::string());
|
||||
@@ -3476,6 +3517,14 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
new_variant_counts = old_variant_counts;
|
||||
//filament_variant_count = old_variant_counts;
|
||||
//ORCA: lay the per-variant options out one value per variant of the current filaments before each
|
||||
// loaded filament replaces its own variants, including an option only a loaded filament
|
||||
// defines, which otherwise starts as a single default value and never reaches the others.
|
||||
for (const DynamicPrintConfig &config : load_filaments_config)
|
||||
for (const std::string &opt_key : filament_options_with_variant)
|
||||
if (opt_key != "filament_extruder_variant" && config.has(opt_key))
|
||||
m_print_config.option(opt_key, true);
|
||||
normalize_filament_values_to_variants(m_print_config);
|
||||
for (int index = 0; index < load_filaments_config.size(); index++) {
|
||||
DynamicPrintConfig& config = load_filaments_config[index];
|
||||
int filament_index = load_filaments_index[index];
|
||||
@@ -3647,6 +3696,25 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
//ORCA: a per-variant option the loaded filament does not define keeps the values of the
|
||||
// variants the filament already had, and a variant new to it takes its first one's.
|
||||
const int old_start = old_start_indice[filament_index - 1];
|
||||
std::vector<int> kept_variant_indice = new_variant_indice;
|
||||
for (int &i : kept_variant_indice)
|
||||
if (i < 0)
|
||||
i = old_start;
|
||||
for (const std::string &opt_key : filament_options_with_variant) {
|
||||
if (config.has(opt_key))
|
||||
continue;
|
||||
auto *opt_vec_dst = dynamic_cast<ConfigOptionVectorBase *>(m_print_config.option(opt_key));
|
||||
if (opt_vec_dst == nullptr || opt_vec_dst->size() < size_t(old_start + old_variant_count))
|
||||
continue;
|
||||
// set_with_restore_2() pads its source in place
|
||||
std::unique_ptr<ConfigOption> old_values(opt_vec_dst->clone());
|
||||
opt_vec_dst->set_with_restore_2(static_cast<ConfigOptionVectorBase *>(old_values.get()), kept_variant_indice, old_start,
|
||||
old_variant_count, true);
|
||||
}
|
||||
|
||||
//update the old index
|
||||
if (old_variant_count != new_variant_count)
|
||||
{
|
||||
@@ -4081,6 +4149,9 @@ int CLI::run(int argc, char **argv)
|
||||
if (printer_technology == ptFFF) {
|
||||
fff_print_config.apply(m_print_config, true);
|
||||
m_print_config.apply(fff_print_config, true);
|
||||
//ORCA: an option no preset or project defines has just come in as its single default value, and a
|
||||
// command line override may hold one value per filament.
|
||||
normalize_filament_values_to_variants(m_print_config);
|
||||
} else {
|
||||
boost::nowide::cerr << "invalid printer_technology " << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PRINTER_TECH, 0, cli_errors[CLI_INVALID_PRINTER_TECH], sliced_info);
|
||||
@@ -7290,9 +7361,9 @@ int CLI::run(int argc, char **argv)
|
||||
colors_out[color_idx] = ColorRGBA(float(rgb_color[0]) / 255.f, float(rgb_color[1]) / 255.f, float(rgb_color[2]) / 255.f, float(rgb_color[3]) / 255.f);
|
||||
}
|
||||
|
||||
int gl_major, gl_minor, gl_verbos;
|
||||
glfwGetVersion(&gl_major, &gl_minor, &gl_verbos);
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("opengl version %1%.%2%.%3%")%gl_major %gl_minor %gl_verbos;
|
||||
int glfw_major, glfw_minor, glfw_revision;
|
||||
glfwGetVersion(&glfw_major, &glfw_minor, &glfw_revision);
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("GLFW version %1%.%2%.%3%") % glfw_major % glfw_minor % glfw_revision;
|
||||
|
||||
bool thumbnail_opengl_ready = false;
|
||||
glfwSetErrorCallback(glfw_callback);
|
||||
@@ -7304,8 +7375,9 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(info) << "glfwInit Success."<< std::endl;
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, gl_major);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, gl_minor);
|
||||
// Request OrcaSlicer's minimum OpenGL version, independently of the GLFW library version.
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||
glfwWindowHint(GLFW_RED_BITS, 8);
|
||||
glfwWindowHint(GLFW_GREEN_BITS, 8);
|
||||
glfwWindowHint(GLFW_BLUE_BITS, 8);
|
||||
@@ -7321,6 +7393,16 @@ int CLI::run(int argc, char **argv)
|
||||
#endif
|
||||
|
||||
GLFWwindow* window = glfwCreateWindow(640, 480, "base_window", NULL, NULL);
|
||||
#ifndef __WXMAC__
|
||||
if (window == NULL) {
|
||||
// Some drivers (e.g. older Mesa) only expose compatibility profile 3.0; take whatever they offer.
|
||||
BOOST_LOG_TRIVIAL(warning) << "Failed to create OpenGL 3.3 compatibility context, retrying with driver default" << std::endl;
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 1);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_ANY_PROFILE);
|
||||
window = glfwCreateWindow(640, 480, "base_window", NULL, NULL);
|
||||
}
|
||||
#endif
|
||||
if (window == NULL)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to create GLFW window; skipping thumbnail rendering for CLI export" << std::endl;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/log/core.hpp>
|
||||
@@ -32,6 +33,7 @@
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
using namespace Slic3r;
|
||||
@@ -160,37 +162,62 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d ¢er)
|
||||
return rigid ? move : Vec2d::Zero();
|
||||
}
|
||||
|
||||
// Slice one 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(DynamicPrintConfig cfg, bool is_bbl)
|
||||
// Slice cubes that switch from filament 1 to filament 2 partway up, so a 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. The layer-by-layer slice also pauses at z 2 and runs the template custom g-code at z 7, so
|
||||
// machine_pause_gcode and template_custom_gcode expand, and turns clumping detection on for a printer that
|
||||
// ships wrapping_detection_gcode. The by-object slice prints two cubes in turn without a prime tower:
|
||||
// printing_by_object_gcode fires before the second one, and the filament change goes through set_extruder.
|
||||
// Either way the output file name is built after export, which expands filename_format with the final
|
||||
// print statistics. An undefined placeholder in any shipped custom g-code throws
|
||||
// Slic3r::PlaceholderParserError.
|
||||
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, bool by_object)
|
||||
{
|
||||
const Vec2d center = printable_area_center(cfg);
|
||||
const Vec2d cube_min = center - Vec2d(5., 5.) + place_wipe_tower(cfg, center);
|
||||
TriangleMesh m = make_cube(10, 10, 10);
|
||||
m.translate(static_cast<float>(cube_min.x()), static_cast<float>(cube_min.y()), 0.f);
|
||||
const Vec2d center = printable_area_center(cfg);
|
||||
std::vector<Vec2d> cube_mins;
|
||||
if (by_object) {
|
||||
// By-object printing fires the hook only without a wipe tower, and rules out clumping detection and
|
||||
// smooth timelapse. No skirt, so the two cubes' own skirts cannot overlap.
|
||||
cfg.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
|
||||
cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(false));
|
||||
cfg.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
|
||||
cfg.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
|
||||
cfg.set_key_value("skirt_loops", new ConfigOptionInt(0));
|
||||
cube_mins = {center + Vec2d(-20., -5.), center + Vec2d(10., -5.)};
|
||||
} else {
|
||||
// Clumping detection changes the tower footprint, so turn it on before placing the tower.
|
||||
if (!cfg.opt_string("wrapping_detection_gcode").empty())
|
||||
cfg.set_key_value("enable_wrapping_detection", new ConfigOptionBool(true));
|
||||
cube_mins = {center - Vec2d(5., 5.) + place_wipe_tower(cfg, center)};
|
||||
}
|
||||
|
||||
Model model;
|
||||
Print print;
|
||||
ModelObject *obj = model.add_object();
|
||||
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
|
||||
obj->add_volume(m);
|
||||
obj->add_instance();
|
||||
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
|
||||
DynamicPrintConfig range_config;
|
||||
range_config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
|
||||
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
|
||||
// trips Flow::with_spacing.
|
||||
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
|
||||
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
|
||||
|
||||
print.is_BBL_printer() = is_bbl;
|
||||
obj->ensure_on_bed();
|
||||
print.auto_assign_extruders(obj);
|
||||
for (const Vec2d &cube_min : cube_mins) {
|
||||
TriangleMesh m = make_cube(10, 10, 10);
|
||||
m.translate(static_cast<float>(cube_min.x()), static_cast<float>(cube_min.y()), 0.f);
|
||||
ModelObject *obj = model.add_object();
|
||||
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
|
||||
obj->add_volume(m);
|
||||
obj->add_instance();
|
||||
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
|
||||
DynamicPrintConfig range_config;
|
||||
range_config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
|
||||
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
|
||||
// trips Flow::with_spacing.
|
||||
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
|
||||
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
|
||||
obj->ensure_on_bed();
|
||||
print.auto_assign_extruders(obj);
|
||||
}
|
||||
// Custom g-codes per print_z apply to layer-by-layer printing only (ToolOrdering::assign_custom_gcodes).
|
||||
if (!by_object)
|
||||
model.plates_custom_gcodes[model.curr_plate_index].gcodes = {{2., CustomGCode::PausePrint, 1, "", ""},
|
||||
{7., CustomGCode::Template, 1, "", ""}};
|
||||
print.apply(model, cfg);
|
||||
print.validate();
|
||||
|
||||
@@ -205,6 +232,7 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl)
|
||||
in.close();
|
||||
boost::system::error_code ec;
|
||||
fs::remove(tmp, ec);
|
||||
print.output_filename(); // names the export as the app does, expanding filename_format
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -264,18 +292,119 @@ void install_slice_context_log_sink()
|
||||
logging::core::get()->add_sink(sink);
|
||||
}
|
||||
|
||||
// Size the filament slots for the current selection and build the config every slice uses.
|
||||
DynamicPrintConfig slice_config(PresetBundle &bundle)
|
||||
{
|
||||
// Grow to a 2nd filament so the cube can change colour; never shrink a multi-nozzle printer
|
||||
// below its nozzle count, or full_config()'s flush-volume matrix no longer matches validate().
|
||||
const size_t nozzles = bundle.printers.get_selected_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->size();
|
||||
bundle.set_num_filaments((unsigned int) std::max<size_t>(2, nozzles));
|
||||
|
||||
// Mirror the app's manual filament->nozzle assignment for a multi-nozzle BBL printer: put each
|
||||
// filament on its own nozzle and pin the map (fmmManual) so full_config() collapses every filament to
|
||||
// the variant of the nozzle it actually prints from, and the engine keeps that assignment instead of
|
||||
// auto-remapping it during process(). Without this the synthetic 2nd filament keeps nozzle 1's variant
|
||||
// while the auto map moves it to nozzle 2 - harmless, but on the one printer whose nozzles differ in
|
||||
// type (Direct Drive + Bowden) the mismatched lookup spams [error] lines. Single-nozzle and non-BBL
|
||||
// printers keep the default map (their toolchange rides the AMS/tool-changer path unchanged).
|
||||
const bool pin_filament_map = bundle.is_bbl_vendor() && nozzles > 1;
|
||||
if (pin_filament_map) {
|
||||
auto &fmap = bundle.project_config.option<ConfigOptionInts>("filament_map", true)->values;
|
||||
for (size_t i = 0; i < fmap.size(); ++i)
|
||||
fmap[i] = int(i % nozzles) + 1;
|
||||
}
|
||||
|
||||
DynamicPrintConfig cfg = bundle.full_config();
|
||||
cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); // force a purge tower so the change is detectable
|
||||
// The map above drives full_config()'s per-filament variant collapse; fmmManual on the sliced config
|
||||
// stops process() from auto-remapping filaments back onto a different nozzle (which would re-introduce
|
||||
// the variant mismatch this pinning avoids).
|
||||
if (pin_filament_map)
|
||||
cfg.set_key_value("filament_map_mode", new ConfigOptionEnum<FilamentMapMode>(fmmManual));
|
||||
|
||||
// full_config() grows filament_extruder_variant to one entry per filament, but because the synthetic
|
||||
// 2nd filament is a duplicate of the first (set_num_filaments copies the same preset), it leaves
|
||||
// filament_self_index at size 1. That makes update_values_to_printer_extruders_for_multiple_filaments
|
||||
// fail to resolve the 2nd filament's variant - a benign fallback that spams [error] lines. A real
|
||||
// 2-colour project ships filament_self_index = 1,2,...; mirror that so the sweep log stays clean. The
|
||||
// slice output is unaffected: the duplicated filament's per-variant values are identical to the first.
|
||||
if (auto *variants = cfg.option<ConfigOptionStrings>("filament_extruder_variant")) {
|
||||
auto &self_index = cfg.option<ConfigOptionInts>("filament_self_index", true)->values;
|
||||
if (self_index.size() != variants->size()) {
|
||||
self_index.resize(variants->size());
|
||||
for (size_t i = 0; i < self_index.size(); ++i)
|
||||
self_index[i] = int(i) + 1;
|
||||
}
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Slice the current selection and log any failure against `what` (the printer, and the extra preset if
|
||||
// any). With an outdir, the g-code is also saved there as "<file_base>.gcode". Returns the g-code, or an
|
||||
// empty string on failure.
|
||||
std::string slice_selection(PresetBundle &bundle, const std::string &what, bool by_object, const std::string &outdir, const std::string &file_base)
|
||||
{
|
||||
try {
|
||||
const std::string out = slice_two_color_cube_and_export(slice_config(bundle), bundle.is_bbl_vendor(), by_object);
|
||||
if (!outdir.empty() && !out.empty())
|
||||
save_string_file(fs::path(outdir) / (file_base + ".gcode"), out);
|
||||
if (out.empty() || out.find("G1") == std::string::npos) {
|
||||
BOOST_LOG_TRIVIAL(error) << what << " produced no g-code";
|
||||
return {};
|
||||
}
|
||||
return out;
|
||||
} catch (const std::exception &ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << what << " failed to slice: " << ex.what();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// The templates a preset carries, as (key, text): every non-empty *_gcode text, and filename_format. Each
|
||||
// value of a per-variant key counts, though a slice expands only the values of the variants its printer uses.
|
||||
using Templates = std::set<std::pair<std::string, std::string>>;
|
||||
Templates preset_templates(const DynamicPrintConfig &cfg)
|
||||
{
|
||||
Templates out;
|
||||
for (const std::string &key : cfg.keys()) {
|
||||
if (key != "filename_format" && !boost::algorithm::ends_with(key, "_gcode"))
|
||||
continue;
|
||||
const ConfigOption *opt = cfg.option(key);
|
||||
if (opt->type() == coString) {
|
||||
if (const std::string &text = static_cast<const ConfigOptionString *>(opt)->value; !text.empty())
|
||||
out.emplace(key, text);
|
||||
} else if (opt->type() == coStrings) {
|
||||
for (const std::string &text : static_cast<const ConfigOptionStrings *>(opt)->values)
|
||||
if (!text.empty())
|
||||
out.emplace(key, text);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Slice-and-export a two-colour cube through every shipped printer (optionally scoped to one vendor via
|
||||
// -v). Unlike the static reference/placeholder checks, this expands every custom *_gcode - including
|
||||
// change_filament_gcode at the one filament change - against the printer's fully-resolved config, so
|
||||
// undefined-placeholder / invalid-flow bugs surface here. Reports every offending printer and returns 1
|
||||
// if any failed, 0 otherwise. When outdir is non-empty, each printer's g-code is also written there as
|
||||
// "<vendor>__<printer>.gcode" for manual inspection. The sweep is SEQUENTIAL by necessity:
|
||||
// Print::process() keeps process-global state, so slicing printers concurrently in one process races
|
||||
// even with per-slice Model+Print. Load in validation mode so the vendors are read straight from the -p
|
||||
// profiles dir (no data_dir/system tree) and -v scoping is honoured for free.
|
||||
// undefined-placeholder / invalid-flow bugs surface here. PlaceholderParser::check_inactive_branches is
|
||||
// set for the sweep, so the names in {if} branches a slice does not take must resolve too.
|
||||
//
|
||||
// Each printer is sliced with its own default process and filament. Every other compatible system
|
||||
// process and filament whose templates (filename_format, filament_start_gcode, ...) no slice has expanded
|
||||
// yet is then sliced once on that printer - a filament that compatible_prints limits to another process
|
||||
// with that process - so each template text shipped in any system preset is expanded once. That check runs on the first compatible printer the sweep reaches only, in that printer's hook
|
||||
// contexts: a value-dependent error on another printer, or a hook call site there that sets fewer
|
||||
// variables (a multi-nozzle tool change, say), is not covered. The first printer shipping each
|
||||
// printing_by_object_gcode also slices two cubes by object, the only way that hook fires.
|
||||
//
|
||||
// Reports every failure and returns 1 if any slice failed, 0 otherwise. When outdir is non-empty, each
|
||||
// g-code is also written there as "<vendor>__<printer>[__<preset>].gcode" for manual inspection. The
|
||||
// sweep is SEQUENTIAL by necessity: Print::process() keeps process-global state, so slicing printers
|
||||
// concurrently in one process races even with per-slice Model+Print. Load in validation mode so the
|
||||
// vendors are read straight from the -p profiles dir (no data_dir/system tree) and -v scoping is honoured
|
||||
// for free.
|
||||
int slice_all_printers(const std::string &vendor, const std::string &outdir)
|
||||
{
|
||||
install_slice_context_log_sink();
|
||||
PlaceholderParser::check_inactive_branches = true;
|
||||
|
||||
if (!outdir.empty()) {
|
||||
boost::system::error_code ec;
|
||||
@@ -323,7 +452,16 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir)
|
||||
std::cout << "Slicing " << printers.size() << " printer preset(s)"
|
||||
<< (vendor.empty() ? "" : " for vendor " + vendor) << "..." << std::endl;
|
||||
|
||||
int failures = 0;
|
||||
Templates covered; // the process and filament templates some slice has expanded
|
||||
auto cover = [&covered](const Preset &preset) {
|
||||
const Templates templates = preset_templates(preset.config);
|
||||
covered.insert(templates.begin(), templates.end());
|
||||
};
|
||||
auto is_covered = [&covered](const Preset &preset) {
|
||||
const Templates templates = preset_templates(preset.config);
|
||||
return std::includes(covered.begin(), covered.end(), templates.begin(), templates.end());
|
||||
};
|
||||
int failures = 0, extra_processes = 0, extra_filaments = 0, by_object_slices = 0;
|
||||
for (const auto &[vendor_name, printer] : printers) {
|
||||
g_slice_context = vendor_name + " / " + printer; // tag every engine log line from this slice
|
||||
const bool selected = bundle.printers.select_preset_by_name(printer, /*force=*/true);
|
||||
@@ -347,76 +485,115 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Grow to a 2nd filament so the cube can change colour; never shrink a multi-nozzle printer
|
||||
// below its nozzle count, or full_config()'s flush-volume matrix no longer matches validate().
|
||||
const size_t nozzles = bundle.printers.get_selected_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->size();
|
||||
bundle.set_num_filaments((unsigned int) std::max<size_t>(2, nozzles));
|
||||
|
||||
// Mirror the app's manual filament->nozzle assignment for a multi-nozzle BBL printer: put each
|
||||
// filament on its own nozzle and pin the map (fmmManual) so full_config() collapses every filament to
|
||||
// the variant of the nozzle it actually prints from, and the engine keeps that assignment instead of
|
||||
// auto-remapping it during process(). Without this the synthetic 2nd filament keeps nozzle 1's variant
|
||||
// while the auto map moves it to nozzle 2 - harmless, but on the one printer whose nozzles differ in
|
||||
// type (Direct Drive + Bowden) the mismatched lookup spams [error] lines. Single-nozzle and non-BBL
|
||||
// printers keep the default map (their toolchange rides the AMS/tool-changer path unchanged).
|
||||
const bool pin_filament_map = bundle.is_bbl_vendor() && nozzles > 1;
|
||||
if (pin_filament_map) {
|
||||
auto &fmap = bundle.project_config.option<ConfigOptionInts>("filament_map", true)->values;
|
||||
for (size_t i = 0; i < fmap.size(); ++i)
|
||||
fmap[i] = int(i % nozzles) + 1;
|
||||
}
|
||||
|
||||
DynamicPrintConfig cfg = bundle.full_config();
|
||||
cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); // force a purge tower so the change is detectable
|
||||
// The map above drives full_config()'s per-filament variant collapse; fmmManual on the sliced config
|
||||
// stops process() from auto-remapping filaments back onto a different nozzle (which would re-introduce
|
||||
// the variant mismatch this pinning avoids).
|
||||
if (pin_filament_map)
|
||||
cfg.set_key_value("filament_map_mode", new ConfigOptionEnum<FilamentMapMode>(fmmManual));
|
||||
|
||||
// full_config() grows filament_extruder_variant to one entry per filament, but because the synthetic
|
||||
// 2nd filament is a duplicate of the first (set_num_filaments copies the same preset), it leaves
|
||||
// filament_self_index at size 1. That makes update_values_to_printer_extruders_for_multiple_filaments
|
||||
// fail to resolve the 2nd filament's variant - a benign fallback that spams [error] lines. A real
|
||||
// 2-colour project ships filament_self_index = 1,2,...; mirror that so the sweep log stays clean. The
|
||||
// slice output is unaffected: the duplicated filament's per-variant values are identical to the first.
|
||||
if (auto *variants = cfg.option<ConfigOptionStrings>("filament_extruder_variant")) {
|
||||
auto &self_index = cfg.option<ConfigOptionInts>("filament_self_index", true)->values;
|
||||
if (self_index.size() != variants->size()) {
|
||||
self_index.resize(variants->size());
|
||||
for (size_t i = 0; i < self_index.size(); ++i)
|
||||
self_index[i] = int(i) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const std::string out = slice_two_color_cube_and_export(cfg, bundle.is_bbl_vendor());
|
||||
if (!outdir.empty() && !out.empty()) {
|
||||
const fs::path f = fs::path(outdir) / (sanitize_filename(vendor_name) + "__" + sanitize_filename(printer) + ".gcode");
|
||||
save_string_file(f, out);
|
||||
}
|
||||
if (out.empty() || out.find("G1") == std::string::npos) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer << "\" produced no g-code";
|
||||
++failures;
|
||||
} else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
|
||||
// The filament change never rode the tower, so change_filament_gcode was not exercised.
|
||||
BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer
|
||||
<< "\" sliced but the filament change never fired (no CP TOOLCHANGE START)";
|
||||
++failures;
|
||||
}
|
||||
} catch (const std::exception &ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer << "\" failed to slice: " << ex.what();
|
||||
const std::string print_name = bundle.prints.get_selected_preset_name();
|
||||
const std::string filament_name = bundle.filaments.get_selected_preset_name();
|
||||
const std::string what = "Printer \"" + printer + "\"";
|
||||
const std::string file_base = sanitize_filename(vendor_name) + "__" + sanitize_filename(printer);
|
||||
if (const std::string out = slice_selection(bundle, what, false, outdir, file_base); out.empty())
|
||||
++failures;
|
||||
else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
|
||||
// The filament change never rode the tower, so change_filament_gcode was not exercised.
|
||||
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no CP TOOLCHANGE START)";
|
||||
++failures;
|
||||
}
|
||||
cover(bundle.prints.get_selected_preset());
|
||||
cover(bundle.filaments.get_selected_preset());
|
||||
|
||||
const std::string &by_object_gcode = bundle.printers.get_selected_preset().config.opt_string("printing_by_object_gcode");
|
||||
if (!by_object_gcode.empty() && covered.emplace("printing_by_object_gcode", by_object_gcode).second) {
|
||||
++by_object_slices;
|
||||
g_slice_context = vendor_name + " / " + printer + " / by object";
|
||||
if (slice_selection(bundle, what + " printing by object", true, outdir, file_base + "__by_object").empty())
|
||||
++failures;
|
||||
}
|
||||
|
||||
// The first process of this printer that compatible_prints lets an otherwise incompatible filament
|
||||
// use, judged as update_compatible() would with that process selected; empty if there is none.
|
||||
const PresetWithVendorProfile printer_with_vendor = bundle.printers.get_edited_preset_with_vendor_profile();
|
||||
auto limiting_process = [&bundle, &printer_with_vendor](const Preset &filament) -> std::string {
|
||||
const auto *processes = filament.config.option<ConfigOptionStrings>("compatible_prints");
|
||||
if (processes == nullptr || processes->values.empty())
|
||||
return {};
|
||||
const PresetWithVendorProfile filament_with_vendor = bundle.filaments.get_preset_with_vendor_profile(filament);
|
||||
if (!is_compatible_with_printer(filament_with_vendor, printer_with_vendor))
|
||||
return {};
|
||||
for (const std::string &name : processes->values)
|
||||
if (const Preset *process = bundle.prints.find_preset(name);
|
||||
process != nullptr && process->is_visible && process->is_compatible &&
|
||||
is_compatible_with_print(filament_with_vendor, bundle.prints.get_preset_with_vendor_profile(*process), printer_with_vendor))
|
||||
return name;
|
||||
return {};
|
||||
};
|
||||
// The compatibility flags are still this printer's, from update_compatible() above, which judged the
|
||||
// filaments against the printer's own process. A hidden preset cannot be selected, so it is counted,
|
||||
// not sliced.
|
||||
std::vector<std::pair<const Preset *, std::string>> extras; // (extra preset, process to slice it with)
|
||||
size_t hidden = 0;
|
||||
for (const PresetCollection *presets : {&bundle.prints, &bundle.filaments})
|
||||
for (const Preset &preset : presets->get_presets()) {
|
||||
if (!preset.is_system || preset.is_default)
|
||||
continue;
|
||||
std::string process;
|
||||
if (preset.is_compatible)
|
||||
process = preset.type == Preset::TYPE_PRINT ? preset.name : print_name;
|
||||
else if (preset.type == Preset::TYPE_FILAMENT)
|
||||
process = limiting_process(preset);
|
||||
if (process.empty() || is_covered(preset))
|
||||
continue;
|
||||
if (preset.is_visible)
|
||||
extras.emplace_back(&preset, process);
|
||||
else
|
||||
++hidden;
|
||||
}
|
||||
if (hidden > 0)
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer \"" << printer << "\": " << hidden
|
||||
<< " compatible system preset(s) with unexpanded templates are hidden and were not sliced";
|
||||
|
||||
// Each extra slice swaps one preset into the printer's own selection. update_compatible() is not
|
||||
// run again: it could swap filament slots. The filament change is not required to ride the tower
|
||||
// here (a vase-mode process has none): the printer's own slice above already expanded its hooks.
|
||||
for (const auto &[preset, process] : extras) {
|
||||
if (is_covered(*preset))
|
||||
continue; // an earlier extra slice expanded the same texts
|
||||
const bool is_print = preset->type == Preset::TYPE_PRINT;
|
||||
const std::string &filament = is_print ? filament_name : preset->name;
|
||||
std::string extra = std::string(is_print ? "process" : "filament") + " \"" + preset->name + "\"";
|
||||
if (!is_print && process != print_name)
|
||||
extra += " and process \"" + process + "\"";
|
||||
g_slice_context = vendor_name + " / " + printer + " / " + extra;
|
||||
if (is_print)
|
||||
++extra_processes;
|
||||
else
|
||||
++extra_filaments;
|
||||
cover(*preset);
|
||||
bundle.prints.select_preset_by_name(process, /*force=*/true);
|
||||
bundle.filaments.select_preset_by_name(filament, /*force=*/true);
|
||||
bundle.filament_presets.assign(1, bundle.filaments.get_selected_preset_name());
|
||||
bundle.update_multi_material_filament_presets();
|
||||
// select_preset_by_name() falls back to another preset, and still returns true, when it cannot select this one.
|
||||
if (bundle.prints.get_selected_preset_name() != process || bundle.filaments.get_selected_preset_name() != filament) {
|
||||
BOOST_LOG_TRIVIAL(error) << what << " could not select " << extra;
|
||||
++failures;
|
||||
continue;
|
||||
}
|
||||
if (slice_selection(bundle, what + " with " + extra, false, outdir, file_base + "__" + sanitize_filename(preset->name)).empty())
|
||||
++failures;
|
||||
}
|
||||
// Leave the printer's own selection for the next printer to start from, as without the extra slices.
|
||||
bundle.prints.select_preset_by_name(print_name, /*force=*/true);
|
||||
bundle.filaments.select_preset_by_name(filament_name, /*force=*/true);
|
||||
}
|
||||
g_slice_context.clear();
|
||||
|
||||
const int slices = int(printers.size()) + extra_processes + extra_filaments + by_object_slices;
|
||||
std::cout << "Sliced " << printers.size() << " printer preset(s), " << extra_processes << " more process preset(s), "
|
||||
<< extra_filaments << " more filament preset(s) and " << by_object_slices << " printer(s) by object" << std::endl;
|
||||
if (failures > 0) {
|
||||
std::cout << failures << " of " << printers.size() << " printer preset(s) failed to slice" << std::endl;
|
||||
std::cout << failures << " of " << slices << " slice(s) failed" << std::endl;
|
||||
std::cout << "Validation failed" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << "All " << printers.size() << " printer preset(s) sliced successfully" << std::endl;
|
||||
std::cout << "All " << slices << " slice(s) succeeded" << std::endl;
|
||||
std::cout << "Validation completed successfully" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
@@ -435,8 +612,8 @@ int main(int argc, char* argv[])
|
||||
#endif
|
||||
("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional, all profiles present in the folder will be validated if not specified")
|
||||
("generate_presets,g", po::value<bool>()->default_value(false), "Generate user presets for mock test")
|
||||
("slice,s", po::bool_switch()->default_value(false), "Slice a two-colour cube through every printer to expand all custom g-code (catches placeholder/flow errors that static checks miss). Off unless this flag is present.")
|
||||
("outdir,o", po::value<std::string>()->default_value(""), "With -s, also save each printer's g-code to this folder (as <vendor>__<printer>.gcode) for manual inspection. Optional.")
|
||||
("slice,s", po::bool_switch()->default_value(false), "Slice a two-colour cube through every printer, and through every other system process and filament preset whose templates no printer's own slice expands, so every custom g-code and filename_format shipped is expanded, names in {if} branches not taken included (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 slice's g-code to this folder (as <vendor>__<printer>[__<preset>].gcode) for manual inspection. Optional.")
|
||||
("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
|
||||
|
||||
@@ -511,6 +511,10 @@ void AppConfig::set_defaults()
|
||||
set_bool("enable_high_low_temp_mixed_printing", false);
|
||||
}
|
||||
|
||||
if (get("remember_print_action").empty()) {
|
||||
set_bool("remember_print_action", false);
|
||||
}
|
||||
|
||||
if (get("ignore_ext_filament_in_filament_map").empty()){
|
||||
set_bool("ignore_ext_filament_in_filament_map", false);
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ double getadhesionCoeff(const PrintObject* printObject)
|
||||
}
|
||||
double adhesionCoeff = 1;
|
||||
for (const ModelVolume* modelVolume : objectVolumes) {
|
||||
if (modelVolume->is_precise_seam()) continue; // non-printing helper geometry
|
||||
for (auto iter = extrudersFirstLayer.begin(); iter != extrudersFirstLayer.end(); iter++) {
|
||||
if (modelVolume->extruder_id() == *iter) {
|
||||
if (Model::extruderParamsMap.find(modelVolume->extruder_id()) != Model::extruderParamsMap.end()) {
|
||||
|
||||
@@ -259,6 +259,8 @@ set(lisbslic3r_sources
|
||||
GCode/RetractWhenCrossingPerimeters.hpp
|
||||
GCode/SeamPlacer.cpp
|
||||
GCode/SeamPlacer.hpp
|
||||
GCode/PreciseSeam.cpp
|
||||
GCode/PreciseSeam.hpp
|
||||
#GCodeSender.cpp
|
||||
#GCodeSender.hpp
|
||||
GCode/SmallAreaInfillFlowCompensator.cpp
|
||||
@@ -353,6 +355,7 @@ set(lisbslic3r_sources
|
||||
Optimize/Optimizer.hpp
|
||||
Orient.cpp
|
||||
Orient.hpp
|
||||
ParallelResolve.hpp
|
||||
ParameterUtils.cpp
|
||||
ParameterUtils.hpp
|
||||
pchheader.cpp
|
||||
|
||||
+26
-13
@@ -849,6 +849,19 @@ ConfigSubstitutions ConfigBase::load_from_json(const std::string &file, ForwardC
|
||||
return std::move(substitutions_ctxt.substitutions);
|
||||
}
|
||||
|
||||
// Case-insensitive compare of a JSON key against a fixed ASCII one, without
|
||||
// boost::iequals, whose std::locale() takes a lock the whole process shares in the
|
||||
// MSVC runtime.
|
||||
static bool ascii_iequals(const std::string &key, const char *literal)
|
||||
{
|
||||
auto lower = [](char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; };
|
||||
size_t i = 0;
|
||||
for (; i < key.size() && literal[i] != '\0'; ++ i)
|
||||
if (lower(key[i]) != lower(literal[i]))
|
||||
return false;
|
||||
return i == key.size() && literal[i] == '\0';
|
||||
}
|
||||
|
||||
int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContext& substitution_context, bool load_inherits_to_config, std::map<std::string, std::string>& key_values, std::string& reason)
|
||||
{
|
||||
json j;
|
||||
@@ -916,44 +929,44 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
|
||||
}
|
||||
//parse the json elements
|
||||
for (auto it = j.begin(); it != j.end(); it++) {
|
||||
if (boost::iequals(it.key(),BBL_JSON_KEY_VERSION)) {
|
||||
if (ascii_iequals(it.key(), BBL_JSON_KEY_VERSION)) {
|
||||
key_values.emplace(BBL_JSON_KEY_VERSION, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) {
|
||||
//skip it
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_NAME)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_NAME)) {
|
||||
key_values.emplace(BBL_JSON_KEY_NAME, it.value());
|
||||
if (it.value() == "project_settings")
|
||||
is_project_settings = true;
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_URL)) {
|
||||
key_values.emplace(BBL_JSON_KEY_URL, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_TYPE)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_TYPE)) {
|
||||
key_values.emplace(BBL_JSON_KEY_TYPE, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_SETTING_ID)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_SETTING_ID)) {
|
||||
key_values.emplace(BBL_JSON_KEY_SETTING_ID, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_FILAMENT_ID)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_FILAMENT_ID)) {
|
||||
key_values.emplace(BBL_JSON_KEY_FILAMENT_ID, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_FROM)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_FROM)) {
|
||||
key_values.emplace(BBL_JSON_KEY_FROM, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) {
|
||||
key_values.emplace(BBL_JSON_KEY_DESCRIPTION, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_INSTANTIATION)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_INSTANTIATION)) {
|
||||
key_values.emplace(BBL_JSON_KEY_INSTANTIATION, it.value());
|
||||
}
|
||||
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INHERITS)) {
|
||||
else if (!load_inherits_to_config && ascii_iequals(it.key(), BBL_JSON_KEY_INHERITS)) {
|
||||
key_values.emplace(BBL_JSON_KEY_INHERITS, it.value());
|
||||
}
|
||||
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INCLUDES)) {
|
||||
else if (!load_inherits_to_config && ascii_iequals(it.key(), BBL_JSON_KEY_INCLUDES)) {
|
||||
key_values.emplace(BBL_JSON_KEY_INCLUDES, it.value().dump());
|
||||
} else if (boost::iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) {
|
||||
} else if (ascii_iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) {
|
||||
key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value());
|
||||
} else {
|
||||
t_config_option_key opt_key = it.key();
|
||||
|
||||
@@ -3094,7 +3094,6 @@ static std::vector<Vec2d> cubic_upper_level(double tau, double h, double period,
|
||||
bool FillRectilinear::fill_surface_trapezoidal(
|
||||
const Surface* surface,
|
||||
FillParams params,
|
||||
const std::initializer_list<SweepParams>& sweep_params,
|
||||
Polylines& polylines_out,
|
||||
int Pattern_type) // 0=grid, 1=triangular, 2=stars, 3=cubic
|
||||
{
|
||||
@@ -3126,9 +3125,20 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
expolygon.rotate(-base_angle, rotate_vector.second);
|
||||
}
|
||||
|
||||
// Use extended object bounding box for consistent pattern across layers
|
||||
BoundingBox bb = this->extended_object_bounding_box();
|
||||
const size_t infill_layer_id = (surface->thickness_layers > 0) ? this->layer_id / surface->thickness_layers : this->layer_id;
|
||||
// The triangular family turns by 120 degrees every layer, about the origin of the frame it is built in.
|
||||
const size_t layer_mod = infill_layer_id % 3;
|
||||
const double angle = layer_mod * 2.0 * M_PI / 3.0;
|
||||
|
||||
// Only build the rows over the surface, seen in the frame they are built in.
|
||||
Polygon local = expolygon.contour;
|
||||
if (Pattern_type != 0) {
|
||||
local.translate(-rotate_vector.second.x(), -rotate_vector.second.y());
|
||||
if (layer_mod)
|
||||
local.rotate(-angle);
|
||||
}
|
||||
BoundingBox cover = get_extents(local);
|
||||
cover.offset(period);
|
||||
|
||||
switch (Pattern_type) {
|
||||
case 0: // Grid / Trapezoidal
|
||||
@@ -3148,21 +3158,31 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
// Align bounding box to the grid, phased through the box center so separated infills align
|
||||
// each part on itself (grid_center is the origin for a standalone object / feature off).
|
||||
// Captured before the merge, which grows bb and would otherwise shift its center.
|
||||
BoundingBox bb = this->extended_object_bounding_box();
|
||||
const Point grid_center = bb.center();
|
||||
bb.merge(align_to_grid(bb.min, Point(period, period), grid_center));
|
||||
const coord_t xmin = bb.min.x();
|
||||
const coord_t xmax = bb.max.x();
|
||||
const coord_t ymin = bb.min.y();
|
||||
const coord_t ymax = bb.max.y();
|
||||
|
||||
auto transpose = [&grid_center](const Point &p) {
|
||||
return Point(grid_center.x() + p.y() - grid_center.y(), grid_center.y() + p.x() - grid_center.x());
|
||||
};
|
||||
if (infill_layer_id % 2 == 1)
|
||||
cover = BoundingBox(transpose(cover.min), transpose(cover.max));
|
||||
const coord_t row_spacing = period / 2;
|
||||
const coord_t first_x = xmin + (coord_t(std::floor(double(cover.min.x() - xmin) / period)) - 1) * period;
|
||||
const coord_t last_x = xmin + (coord_t(std::ceil(double(cover.max.x() - xmin) / period)) + 1) * period;
|
||||
const coord_t first_row = coord_t(std::floor(double(cover.min.y() - ymin) / row_spacing)) - 1;
|
||||
const coord_t last_row = coord_t(std::ceil(double(cover.max.y() - ymin) / row_spacing)) + 1;
|
||||
|
||||
// Create the two base row patterns once
|
||||
Polyline base_row_normal;
|
||||
base_row_normal.points.reserve(((xmax - xmin) / period + 1) * 5); // 5 points per trapezoid
|
||||
base_row_normal.points.reserve(((last_x - first_x) / period + 1) * 5); // 5 points per trapezoid
|
||||
Polyline base_row_flipped;
|
||||
base_row_flipped.points.reserve(((xmax - xmin) / period + 1) * 5); // 5 points per trapezoid
|
||||
base_row_flipped.points.reserve(((last_x - first_x) / period + 1) * 5); // 5 points per trapezoid
|
||||
|
||||
// Build complete rows from xmin to xmax
|
||||
for (coord_t x = xmin; x < xmax; x += period) {
|
||||
// Build rows on the same global period grid, limited to the surface cover.
|
||||
for (coord_t x = first_x; x < last_x; x += period) {
|
||||
// Normal row
|
||||
base_row_normal.points.emplace_back(Point(x, d1 / 2)); // P0
|
||||
base_row_normal.points.emplace_back(Point(x + d1 / 2, d1 / 2)); // P1
|
||||
@@ -3178,13 +3198,14 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
}
|
||||
|
||||
// Pre-allocate polylines
|
||||
const size_t estimated_rows = ((ymax - ymin) / (period / 2) + 1);
|
||||
const size_t estimated_rows = size_t(last_row - first_row + 1);
|
||||
polylines.reserve(estimated_rows);
|
||||
|
||||
bool flip_vertical = false;
|
||||
bool flip_vertical = (first_row % 2) != 0;
|
||||
|
||||
// Now just copy and translate vertically
|
||||
for (coord_t y = ymin; y < ymax; y += period / 2) {
|
||||
// Copy and translate only rows intersecting the surface cover.
|
||||
for (coord_t row = first_row; row <= last_row; ++row) {
|
||||
const coord_t y = ymin + row * row_spacing;
|
||||
Polyline pl_row = flip_vertical ? base_row_flipped : base_row_normal;
|
||||
|
||||
// Translate all points vertically
|
||||
@@ -3200,16 +3221,10 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
// Orca: mirror across the diagonal through grid_center (not the origin), so the swapped
|
||||
// layers stay aligned with the center-phased grid. For a standalone object / feature off,
|
||||
// grid_center is the origin and this is a plain x/y swap.
|
||||
if (infill_layer_id % 2 == 1) {
|
||||
for (Polyline& pl : polylines) {
|
||||
for (Point& p : pl.points) {
|
||||
const coord_t dx = p.x() - grid_center.x();
|
||||
const coord_t dy = p.y() - grid_center.y();
|
||||
p.x() = grid_center.x() + dy;
|
||||
p.y() = grid_center.y() + dx;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (infill_layer_id % 2 == 1)
|
||||
for (Polyline& pl : polylines)
|
||||
for (Point& p : pl.points)
|
||||
p = transpose(p);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3230,40 +3245,24 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
const coord_t d2_tri = coord_t(2.0 / std::sqrt(3.0) * d1);
|
||||
const coord_t h = coord_t(0.5 * std::sqrt(3.0) * period); // height of triangle
|
||||
|
||||
// Align bounding box to the grid
|
||||
bb.merge(align_to_grid(bb.center(), Point(period,h)));
|
||||
const size_t layer_mod = infill_layer_id % 3;
|
||||
const double angle = layer_mod * 2.0 * M_PI / 3.0;
|
||||
|
||||
const Point rotation_center = bb.center();
|
||||
const coord_t half_w = bb.size().x() / 2;
|
||||
const coord_t half_h = bb.size().y() / 2;
|
||||
|
||||
// Compute how many full periods fit in each direction
|
||||
const coord_t num_periods_x = coord_t(std::ceil(half_w / double(period)));
|
||||
coord_t num_periods_y =coord_t(std::ceil(half_h / double(h)));
|
||||
// Ensure an even number of rows so the pattern stays centered
|
||||
if ((num_periods_y % 2) != 0)
|
||||
++num_periods_y;
|
||||
|
||||
// Compute aligned limits (symmetric around the origin)
|
||||
const coord_t x_min_aligned = -num_periods_x * period;
|
||||
const coord_t x_max_aligned = num_periods_x * period;
|
||||
const coord_t y_min_aligned = -num_periods_y * h;
|
||||
const coord_t y_max_aligned = num_periods_y * h;
|
||||
// Keep the existing origin-anchored lattice, but generate only nearby tiles.
|
||||
const coord_t x_min_aligned = (coord_t(std::floor(double(cover.min.x()) / period)) - 1) * period;
|
||||
const coord_t x_max_aligned = (coord_t(std::ceil(double(cover.max.x()) / period)) + 1) * period;
|
||||
const coord_t first_row = coord_t(std::floor(double(cover.min.y()) / h)) - 1;
|
||||
const coord_t last_row = coord_t(std::ceil(double(cover.max.y()) / h)) + 1;
|
||||
|
||||
// Pre-allocate estimated number of polylines
|
||||
const size_t estimated_rows = (y_max_aligned - y_min_aligned) / h + 2;
|
||||
const size_t estimated_polylines = (estimated_rows + 1) * 2; // base line + trapezoid line per row
|
||||
const size_t estimated_rows = size_t(last_row - first_row + 1);
|
||||
const size_t estimated_polylines = estimated_rows * 2; // base line + trapezoid line per row
|
||||
polylines.reserve(estimated_polylines);
|
||||
|
||||
// Create the two base row templates once
|
||||
Polyline base_line_template;
|
||||
base_line_template.points.reserve(2); // 2 points for base line
|
||||
Polyline trapezoid_row_normal;
|
||||
trapezoid_row_normal.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 5); // 5 points per trapezoid
|
||||
trapezoid_row_normal.points.reserve(((x_max_aligned - x_min_aligned) / period) * 5); // 5 points per trapezoid
|
||||
Polyline trapezoid_row_shifted;
|
||||
trapezoid_row_shifted.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 5); // 5 points per trapezoid
|
||||
trapezoid_row_shifted.points.reserve(((x_max_aligned - x_min_aligned) / period) * 5); // 5 points per trapezoid
|
||||
// Build base line template (from x_min_aligned to x_max_aligned)
|
||||
base_line_template.points.emplace_back(Point(x_min_aligned, 0));
|
||||
base_line_template.points.emplace_back(Point(x_max_aligned, 0));
|
||||
@@ -3283,10 +3282,10 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
for (auto& p : trapezoid_row_shifted.points)
|
||||
p.y() = h - p.y();
|
||||
|
||||
bool shift_row = false;
|
||||
|
||||
// Generate pattern by copying and translating templates vertically
|
||||
for (coord_t y = y_min_aligned; y < y_max_aligned; y += h) {
|
||||
bool shift_row = (first_row % 2) != 0;
|
||||
for (coord_t row = first_row; row <= last_row; ++row) {
|
||||
const coord_t y = row * h;
|
||||
// Base line - copy and translate
|
||||
Polyline base_line = base_line_template;
|
||||
for (Point& p : base_line.points) {
|
||||
@@ -3306,12 +3305,6 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
|
||||
shift_row = !shift_row;
|
||||
}
|
||||
|
||||
// Rotate around origin (0,0)
|
||||
if (layer_mod)
|
||||
for (auto& pl : polylines)
|
||||
pl.rotate(angle, Point(0,0));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3325,36 +3318,25 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
const coord_t d1_half_base = d1_half / std::sqrt(3.0);
|
||||
const coord_t half_period = period / 2;
|
||||
const coord_t quarter_period = period / 4;
|
||||
const coord_t row_y_offset = tri_height - (2 * tri_height) / 3;
|
||||
|
||||
bb.merge(align_to_grid(bb.center(), Point(period, tri_height)));
|
||||
const size_t layer_mod = infill_layer_id % 3;
|
||||
const double angle = layer_mod * 2.0 * M_PI / 3.0;
|
||||
// Keep the lattice anchored at the origin while generating only tiles around this surface.
|
||||
const int64_t first_tile = int64_t(std::floor(double(cover.min.x()) / period)) - 1;
|
||||
const int64_t last_tile = int64_t(std::ceil(double(cover.max.x()) / period)) + 1;
|
||||
const int64_t first_row = int64_t(std::floor(double(cover.min.y() - row_y_offset) / hex_height)) - 1;
|
||||
const int64_t last_row = int64_t(std::ceil(double(cover.max.y() - row_y_offset) / hex_height)) + 1;
|
||||
|
||||
const coord_t half_w = bb.size().x() / 2;
|
||||
const coord_t half_h = bb.size().y() / 2;
|
||||
|
||||
const coord_t num_periods_x = coord_t(std::ceil(half_w / double(period)));
|
||||
coord_t num_periods_y = coord_t(std::ceil(half_h / double(hex_height)));
|
||||
if ((num_periods_y % 2) != 0)
|
||||
++num_periods_y;
|
||||
|
||||
const coord_t x_alignment_shift = half_period;
|
||||
const coord_t y_alignment_shift = (2 * tri_height) / 3;
|
||||
const coord_t x_min_aligned = -num_periods_x * period - x_alignment_shift;
|
||||
const coord_t x_max_aligned = num_periods_x * period - x_alignment_shift;
|
||||
const coord_t y_min_aligned = -num_periods_y * hex_height - y_alignment_shift;
|
||||
const coord_t y_max_aligned = num_periods_y * hex_height - y_alignment_shift;
|
||||
|
||||
const size_t estimated_rows = (y_max_aligned - y_min_aligned) / hex_height + 2;
|
||||
const size_t estimated_polylines = (estimated_rows + 1) * 2;
|
||||
const size_t estimated_rows = size_t(last_row - first_row + 1);
|
||||
const size_t estimated_polylines = estimated_rows * 2;
|
||||
polylines.reserve(estimated_polylines);
|
||||
|
||||
Polyline star_row_normal;
|
||||
star_row_normal.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 7);
|
||||
star_row_normal.points.reserve(size_t(last_tile - first_tile) * 7);
|
||||
Polyline star_row_mirrored;
|
||||
star_row_mirrored.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 7);
|
||||
star_row_mirrored.points.reserve(size_t(last_tile - first_tile) * 7);
|
||||
|
||||
for (coord_t x = x_min_aligned; x < x_max_aligned; x += period) {
|
||||
for (int64_t tile = first_tile; tile < last_tile; ++tile) {
|
||||
const coord_t x = coord_t(tile * period) - half_period;
|
||||
star_row_normal.points.emplace_back(Point(x, hex_height)); // P0
|
||||
star_row_normal.points.emplace_back(Point(x + quarter_period - d1, hex_height)); // P1
|
||||
star_row_normal.points.emplace_back(Point(x + quarter_period + d1_half, hex_height - chamfer_height)); // P2
|
||||
@@ -3368,9 +3350,7 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
for (auto& p : star_row_mirrored.points)
|
||||
p.y() = hex_height - p.y();
|
||||
|
||||
size_t pair_idx = 0;
|
||||
const coord_t global_x_shift = half_period;
|
||||
const coord_t global_y_shift = tri_height;
|
||||
auto append_row_with_shift = [&polylines](const Polyline& row_template, coord_t x_shift, coord_t y_shift) {
|
||||
Polyline row = row_template;
|
||||
for (Point& p : row.points) {
|
||||
@@ -3381,16 +3361,12 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
polylines.emplace_back(std::move(row));
|
||||
};
|
||||
|
||||
for (coord_t y = y_min_aligned; y < y_max_aligned; y += hex_height, ++pair_idx) {
|
||||
const coord_t x_shift = (pair_idx % 2 == 0) ? 0 : half_period;
|
||||
append_row_with_shift(star_row_normal, x_shift + global_x_shift, y + global_y_shift);
|
||||
append_row_with_shift(star_row_mirrored, x_shift + global_x_shift, y + global_y_shift);
|
||||
for (int64_t row = first_row; row <= last_row; ++row) {
|
||||
const coord_t y = coord_t(row * hex_height) + row_y_offset;
|
||||
const coord_t x_shift = (row % 2 == 0) ? 0 : half_period;
|
||||
append_row_with_shift(star_row_normal, x_shift + global_x_shift, y);
|
||||
append_row_with_shift(star_row_mirrored, x_shift + global_x_shift, y);
|
||||
}
|
||||
|
||||
if (layer_mod)
|
||||
for (auto& pl : polylines)
|
||||
pl.rotate(angle, Point(0, 0));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3407,19 +3383,9 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
for (Vec2d &p : levels.front())
|
||||
p.y() = h - p.y();
|
||||
|
||||
const size_t layer_mod = infill_layer_id % 3;
|
||||
const double angle = layer_mod * 2.0 * M_PI / 3.0;
|
||||
|
||||
// Only cover the surface, seen in the frame the pattern is built in.
|
||||
ExPolygon local = expolygon;
|
||||
local.translate(-rotate_vector.second.x(), -rotate_vector.second.y());
|
||||
if (layer_mod)
|
||||
local.rotate(-angle);
|
||||
BoundingBox cover = get_extents(local);
|
||||
cover.offset(period);
|
||||
|
||||
const int64_t n_min = int64_t(std::floor((cover.min.y() - y0) / h)) - 1;
|
||||
const int64_t n_max = int64_t(std::ceil((cover.max.y() - y0) / h)) + 1;
|
||||
polylines.reserve(size_t(n_max - n_min + 1) * levels.size());
|
||||
for (int64_t n = n_min; n <= n_max; ++n) {
|
||||
const double x_off = (n & 1) ? 0.5 * period : 0.;
|
||||
const double base = y0 + double(n) * h - tau;
|
||||
@@ -3435,11 +3401,6 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
polylines.emplace_back(std::move(row));
|
||||
}
|
||||
}
|
||||
|
||||
if (layer_mod)
|
||||
for (Polyline &pl : polylines)
|
||||
pl.rotate(angle, Point(0, 0));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3448,21 +3409,38 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
break;
|
||||
}
|
||||
|
||||
// Orca: cases 1 & 2 build the pattern symmetrically around the origin, so on their own they
|
||||
// phase to the global origin and every part shares one grid. Shift the pattern onto the box
|
||||
// center this->bounding_box carries, so separated infills align each part on itself. The center
|
||||
// is the origin for a standalone object (or when the feature is off), making this a no-op there.
|
||||
// Orca: cases 1 to 3 anchor the pattern at the origin, so on their own they phase to the global
|
||||
// origin and every part shares one grid. Shift the pattern onto the box center
|
||||
// this->bounding_box carries, so separated infills align each part on itself. The center is the
|
||||
// origin for a standalone object (or when the feature is off), making the shift a no-op there.
|
||||
if (Pattern_type != 0)
|
||||
for (Polyline &pl : polylines)
|
||||
for (Polyline &pl : polylines) {
|
||||
if (layer_mod)
|
||||
pl.rotate(angle, Point(0, 0));
|
||||
pl.translate(rotate_vector.second);
|
||||
}
|
||||
|
||||
// Orca: round the corners of the trapezoids. The straight base lines of the triangular family
|
||||
// have no corner to round.
|
||||
smooth_polylines_corners(polylines, params.smooth_factor, scaled<double>(params.resolution));
|
||||
|
||||
// Only the centerlines within d1 / 2 of the surface have outlines reaching it.
|
||||
polylines = intersection_pl(std::move(polylines), offset(expolygon, float(d1 / 2)));
|
||||
|
||||
// Apply multiline fill
|
||||
multiline_fill(polylines, params, spacing);
|
||||
|
||||
// Start each outline on the cap at the first end of its path, outside the surface, so clipping splits it only there.
|
||||
const Vec2d row_dir = Pattern_type != 0 ? Vec2d(std::cos(angle), std::sin(angle)) : infill_layer_id % 2 ? Vec2d::UnitY() : Vec2d::UnitX();
|
||||
for (Polyline &pl : polylines)
|
||||
if (pl.size() > 3 && pl.first_point() == pl.last_point()) {
|
||||
pl.points.pop_back();
|
||||
std::rotate(pl.points.begin(), std::min_element(pl.points.begin(), pl.points.end(), [&row_dir](const Point &a, const Point &b) {
|
||||
return row_dir.dot(a.cast<double>()) < row_dir.dot(b.cast<double>());
|
||||
}), pl.points.end());
|
||||
pl.points.emplace_back(pl.points.front());
|
||||
}
|
||||
|
||||
// Contract surface polygon by half line width to avoid excesive overlap with perimeter
|
||||
ExPolygons contracted = offset_ex(expolygon, -float(scale_(0.5 * this->spacing)));
|
||||
|
||||
@@ -3545,9 +3523,7 @@ Polylines FillGrid::fill_surface(const Surface *surface, const FillParams ¶m
|
||||
if (params.multiline > 1) {
|
||||
// Experimental trapezoidal grid
|
||||
if (!this->fill_surface_trapezoidal(
|
||||
surface, params,
|
||||
{ { 0.f, 0.f }, { float(M_PI / 2.), 0.f } },
|
||||
polylines_out,0))
|
||||
surface, params, polylines_out, 0))
|
||||
BOOST_LOG_TRIVIAL(error) << "FillGrid::fill_surface_trapezoidal() failed.";
|
||||
|
||||
} else {
|
||||
@@ -3587,9 +3563,7 @@ Polylines FillTriangles::fill_surface(const Surface *surface, const FillParams &
|
||||
if (params.multiline > 1) {
|
||||
// Experimental trapezoidal grid
|
||||
if (!this->fill_surface_trapezoidal(
|
||||
surface, params,
|
||||
{ { 0.f, 0.f }, { float(M_PI / 2.), 0.f } },
|
||||
polylines_out,1))
|
||||
surface, params, polylines_out, 1))
|
||||
BOOST_LOG_TRIVIAL(error) << "FillGrid::fill_surface_trapezoidal() failed.";
|
||||
|
||||
} else {
|
||||
@@ -3608,9 +3582,7 @@ Polylines FillStars::fill_surface(const Surface *surface, const FillParams ¶
|
||||
Polylines polylines_out;
|
||||
if (params.multiline > 1) {
|
||||
if (!this->fill_surface_trapezoidal(
|
||||
surface, params,
|
||||
{{0.f, 0.f}, {float(M_PI / 3.), 0.f}, {float(2. * M_PI / 3.), float((3. / 2.) * this->spacing * params.multiline / params.density)}},
|
||||
polylines_out, 2))
|
||||
surface, params, polylines_out, 2))
|
||||
BOOST_LOG_TRIVIAL(error) << "FillStars::fill_surface_trapezoidal() failed.";
|
||||
} else {
|
||||
if (! this->fill_surface_by_multilines(
|
||||
@@ -3626,7 +3598,7 @@ Polylines FillCubic::fill_surface(const Surface *surface, const FillParams ¶
|
||||
{
|
||||
Polylines polylines_out;
|
||||
if (params.multiline > 1) {
|
||||
if (!this->fill_surface_trapezoidal(surface, params, {}, polylines_out, 3))
|
||||
if (!this->fill_surface_trapezoidal(surface, params, polylines_out, 3))
|
||||
BOOST_LOG_TRIVIAL(error) << "FillCubic::fill_surface_trapezoidal() failed.";
|
||||
return polylines_out;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ protected:
|
||||
float pattern_shift;
|
||||
};
|
||||
bool fill_surface_by_multilines(const Surface *surface, FillParams params, const std::initializer_list<SweepParams> &sweep_params, Polylines &polylines_out);
|
||||
bool fill_surface_trapezoidal(const Surface *surface, FillParams params, const std::initializer_list<SweepParams> &sweep_params, Polylines &polylines_out,int Pattern_type);
|
||||
bool fill_surface_trapezoidal(const Surface *surface, FillParams params, Polylines &polylines_out, int Pattern_type);
|
||||
|
||||
// The extended bounding box of the whole object that covers any rotation of every layer.
|
||||
BoundingBox extended_object_bounding_box() const;
|
||||
|
||||
@@ -125,6 +125,10 @@ static constexpr const char* VOLUME_TYPE = "volume";
|
||||
static constexpr const char* NAME_KEY = "name";
|
||||
static constexpr const char* MODIFIER_KEY = "modifier";
|
||||
static constexpr const char* VOLUME_TYPE_KEY = "volume_type";
|
||||
// Keep seam modes separate from the base type so older readers see a non-printing modifier.
|
||||
static constexpr const char* PRECISE_SEAM_TYPE_KEY = "precise_seam_type";
|
||||
// Preserve dormant settings without turning an older reader's modifier into an active override.
|
||||
static constexpr char PRECISE_SEAM_CONFIG_PREFIX[] = "precise_seam_config:";
|
||||
static constexpr const char* MATRIX_KEY = "matrix";
|
||||
static constexpr const char* SOURCE_FILE_KEY = "source_file";
|
||||
static constexpr const char* SOURCE_OBJECT_ID_KEY = "source_object_id";
|
||||
@@ -307,14 +311,17 @@ bool PrusaFileParser::check_3mf_from_prusa(const std::string filename)
|
||||
|
||||
mz_zip_archive_file_stat stat;
|
||||
if (!mz_zip_reader_file_stat(&archive, model_file_index, &stat)) goto EXIT;
|
||||
// expat sizes its buffer with an int, so a larger entry cannot be parsed in one piece.
|
||||
if (stat.m_uncomp_size > static_cast<mz_uint64>(std::numeric_limits<int>::max())) goto EXIT;
|
||||
|
||||
void *parser_buffer = XML_GetBuffer(m_parser, (int) stat.m_uncomp_size);
|
||||
const int xml_size = static_cast<int>(stat.m_uncomp_size);
|
||||
void *parser_buffer = XML_GetBuffer(m_parser, xml_size);
|
||||
if (parser_buffer == nullptr) goto EXIT;
|
||||
|
||||
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, (size_t) stat.m_uncomp_size, 0);
|
||||
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, static_cast<size_t>(xml_size), 0);
|
||||
if (res == 0) goto EXIT;
|
||||
|
||||
XML_ParseBuffer(m_parser, (int) stat.m_uncomp_size, 1);
|
||||
XML_ParseBuffer(m_parser, xml_size, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +375,14 @@ ModelVolumeType type_from_string(const std::string &s)
|
||||
if (s == "ParameterModifier") return ModelVolumeType::PARAMETER_MODIFIER;
|
||||
if (s == "SupportEnforcer") return ModelVolumeType::SUPPORT_ENFORCER;
|
||||
if (s == "SupportBlocker") return ModelVolumeType::SUPPORT_BLOCKER;
|
||||
// Default value if invalud type string received.
|
||||
// Precise Seam types (snake_case strings from ModelVolume::type_to_string)
|
||||
if (s == "precise_seam_center") return ModelVolumeType::PRECISE_SEAM_CENTER;
|
||||
if (s == "precise_seam_left") return ModelVolumeType::PRECISE_SEAM_LEFT;
|
||||
if (s == "precise_seam_right") return ModelVolumeType::PRECISE_SEAM_RIGHT;
|
||||
if (s == "precise_seam_enforced") return ModelVolumeType::PRECISE_SEAM_ENFORCED;
|
||||
if (s == "precise_seam_blocked") return ModelVolumeType::PRECISE_SEAM_BLOCKED;
|
||||
if (s == "precise_seam_neutral") return ModelVolumeType::PRECISE_SEAM_NEUTRAL;
|
||||
// Default value if invalid type string received.
|
||||
return ModelVolumeType::MODEL_PART;
|
||||
}
|
||||
|
||||
@@ -1346,19 +1360,26 @@ ModelVolumeType type_from_string(const std::string &s)
|
||||
XML_SetUserData(m_xml_parser, (void*)this);
|
||||
XML_SetElementHandler(m_xml_parser, _3MF_Importer::_handle_start_config_xml_element, _3MF_Importer::_handle_end_config_xml_element);
|
||||
|
||||
void* parser_buffer = XML_GetBuffer(m_xml_parser, (int)stat.m_uncomp_size);
|
||||
// expat sizes its buffer with an int, so a larger entry cannot be parsed in one piece.
|
||||
if (stat.m_uncomp_size > static_cast<mz_uint64>(std::numeric_limits<int>::max())) {
|
||||
add_error("Found invalid size");
|
||||
return false;
|
||||
}
|
||||
const int xml_size = static_cast<int>(stat.m_uncomp_size);
|
||||
|
||||
void* parser_buffer = XML_GetBuffer(m_xml_parser, xml_size);
|
||||
if (parser_buffer == nullptr) {
|
||||
add_error("Unable to create buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, (size_t)stat.m_uncomp_size, 0);
|
||||
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, static_cast<size_t>(xml_size), 0);
|
||||
if (res == 0) {
|
||||
add_error("Error while reading config data to buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!XML_ParseBuffer(m_xml_parser, (int)stat.m_uncomp_size, 1)) {
|
||||
if (!XML_ParseBuffer(m_xml_parser, xml_size, 1)) {
|
||||
char error_buf[1024];
|
||||
::sprintf(error_buf, "Error (%s) while parsing xml file at line %d", XML_ErrorString(XML_GetErrorCode(m_xml_parser)), (int)XML_GetCurrentLineNumber(m_xml_parser));
|
||||
add_error(error_buf);
|
||||
@@ -2035,6 +2056,7 @@ ModelVolumeType type_from_string(const std::string &s)
|
||||
std::vector<std::string> valid_keys = {
|
||||
"name",
|
||||
"volume_type",
|
||||
PRECISE_SEAM_TYPE_KEY,
|
||||
"matrix",
|
||||
"source_file",
|
||||
"source_object_id",
|
||||
@@ -2047,7 +2069,7 @@ ModelVolumeType type_from_string(const std::string &s)
|
||||
};
|
||||
|
||||
auto itor = std::find(valid_keys.begin(), valid_keys.end(), key);
|
||||
if (itor == valid_keys.end()) {
|
||||
if (itor == valid_keys.end() && !(type == VOLUME_TYPE && boost::starts_with(key, PRECISE_SEAM_CONFIG_PREFIX))) {
|
||||
// do nothing if not valid keys
|
||||
return true;
|
||||
}
|
||||
@@ -2180,6 +2202,8 @@ ModelVolumeType type_from_string(const std::string &s)
|
||||
volume->mmu_segmentation_facets.shrink_to_fit();
|
||||
volume->fuzzy_skin_facets.shrink_to_fit();
|
||||
|
||||
// Apply the seam mode after all base-type metadata, regardless of XML key order.
|
||||
ModelVolumeType precise_seam_type = ModelVolumeType::INVALID;
|
||||
// apply the remaining volume's metadata
|
||||
for (const Metadata& metadata : volume_data.metadata) {
|
||||
if (metadata.key == NAME_KEY)
|
||||
@@ -2188,6 +2212,10 @@ ModelVolumeType type_from_string(const std::string &s)
|
||||
volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
||||
else if (metadata.key == VOLUME_TYPE_KEY)
|
||||
volume->set_type(type_from_string(metadata.value));
|
||||
else if (metadata.key == PRECISE_SEAM_TYPE_KEY)
|
||||
precise_seam_type = ModelVolume::type_from_string(metadata.value);
|
||||
else if (boost::starts_with(metadata.key, PRECISE_SEAM_CONFIG_PREFIX))
|
||||
continue; // Restore dormant settings only after the final volume type is known.
|
||||
else if (metadata.key == SOURCE_FILE_KEY)
|
||||
volume->source.input_file = metadata.value;
|
||||
else if (metadata.key == SOURCE_OBJECT_ID_KEY)
|
||||
@@ -2208,6 +2236,22 @@ ModelVolumeType type_from_string(const std::string &s)
|
||||
volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions);
|
||||
}
|
||||
|
||||
// Missing or unknown seam modes retain the ordinary modifier fallback.
|
||||
// Ignore seam metadata on other base types; legacy inline seam types still load above.
|
||||
if (volume->is_modifier() && is_precise_seam(precise_seam_type))
|
||||
volume->set_type(precise_seam_type);
|
||||
|
||||
// Unknown seam modes must remain inert modifiers, even when dormant settings are present.
|
||||
if (volume->is_precise_seam()) {
|
||||
for (const Metadata& metadata : volume_data.metadata) {
|
||||
if (boost::starts_with(metadata.key, PRECISE_SEAM_CONFIG_PREFIX)) {
|
||||
const std::string key = metadata.key.substr(sizeof(PRECISE_SEAM_CONFIG_PREFIX) - 1);
|
||||
if (!key.empty())
|
||||
volume->config.set_deserialize(key, metadata.value, config_substitutions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this may happen for 3mf saved by 3rd part softwares
|
||||
if (volume->name.empty()) {
|
||||
volume->name = object.name;
|
||||
@@ -3123,11 +3167,18 @@ ModelVolumeType type_from_string(const std::string &s)
|
||||
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << NAME_KEY << "\" " << VALUE_ATTR << "=\"" << xml_escape(volume->name) << "\"/>\n";
|
||||
|
||||
// stores volume's modifier field (legacy, to support old slicers)
|
||||
if (volume->is_modifier())
|
||||
// Readers with only the legacy flag still see helper geometry as a modifier.
|
||||
if (volume->is_modifier() || volume->is_precise_seam())
|
||||
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MODIFIER_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n";
|
||||
// stores volume's type (overrides the modifier field above)
|
||||
// This Prusa-format reader uses ParameterModifier, not Bambu's modifier_part.
|
||||
// The base type overrides the legacy flag, so it must also be backward-compatible.
|
||||
// Use the same spelling for ordinary modifiers, including a downgraded seam helper.
|
||||
const bool store_as_modifier = volume->is_modifier() || volume->is_precise_seam();
|
||||
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << VOLUME_TYPE_KEY << "\" " <<
|
||||
VALUE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\"/>\n";
|
||||
VALUE_ATTR << "=\"" << (store_as_modifier ? "ParameterModifier" : ModelVolume::type_to_string(volume->type())) << "\"/>\n";
|
||||
if (volume->is_precise_seam())
|
||||
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << PRECISE_SEAM_TYPE_KEY << "\" " <<
|
||||
VALUE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\"/>\n";
|
||||
|
||||
// stores volume's local matrix
|
||||
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MATRIX_KEY << "\" " << VALUE_ATTR << "=\"";
|
||||
@@ -3162,7 +3213,12 @@ ModelVolumeType type_from_string(const std::string &s)
|
||||
|
||||
// stores volume's config data
|
||||
for (const std::string& key : volume->config.keys()) {
|
||||
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n";
|
||||
// Seam settings are inactive but must survive changing the helper back into a part/modifier.
|
||||
const bool dormant = volume->is_precise_seam();
|
||||
const std::string stored_key = dormant ? PRECISE_SEAM_CONFIG_PREFIX + key : key;
|
||||
const std::string value = volume->config.opt_serialize(key);
|
||||
// Config serialization is C-style, not XML: escape active settings too, including tabs.
|
||||
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << stored_key << "\" " << VALUE_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(value) << "\"/>\n";
|
||||
}
|
||||
|
||||
// stores mesh's statistics
|
||||
|
||||
@@ -48,24 +48,37 @@ bool load_drc(const char *path, TriangleMesh *meshptr)
|
||||
indexed_triangle_set its;
|
||||
|
||||
const PointAttribute *const positions = dracoMesh.GetNamedAttribute(GeometryAttribute::POSITION);
|
||||
if (positions == nullptr) {
|
||||
BOOST_LOG_TRIVIAL(error) << "load_drc: the mesh has no POSITION attribute";
|
||||
return false;
|
||||
}
|
||||
size_t num_vertices = positions->size();
|
||||
its.vertices.reserve(num_vertices);
|
||||
for (AttributeValueIndex i(0); i < num_vertices; ++ i) {
|
||||
float pos[3];
|
||||
positions->ConvertValue<float>(i, 3, pos);
|
||||
if (!positions->ConvertValue<float>(i, 3, pos)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "load_drc: invalid vertex position";
|
||||
return false;
|
||||
}
|
||||
its.vertices.emplace_back(pos[0], pos[1], pos[2]);
|
||||
}
|
||||
|
||||
// The Draco decoder does not check face indices against the point count.
|
||||
const uint32_t num_points = dracoMesh.num_points();
|
||||
size_t num_faces = dracoMesh.num_faces();
|
||||
its.indices.reserve(num_faces);
|
||||
for (FaceIndex i(0); i < num_faces; ++ i) {
|
||||
Mesh::Face face = dracoMesh.face(i);
|
||||
|
||||
its.indices.emplace_back(
|
||||
positions->mapped_index(face[0]).value(),
|
||||
positions->mapped_index(face[1]).value(),
|
||||
positions->mapped_index(face[2]).value()
|
||||
);
|
||||
const Mesh::Face &face = dracoMesh.face(i);
|
||||
stl_triangle_vertex_indices facet;
|
||||
for (int k = 0; k < 3; ++ k) {
|
||||
const size_t vertex_idx = face[k].value() < num_points ? positions->mapped_index(face[k]).value() : num_vertices;
|
||||
if (vertex_idx >= num_vertices) {
|
||||
BOOST_LOG_TRIVIAL(error) << "load_drc: invalid vertex index";
|
||||
return false;
|
||||
}
|
||||
facet[k] = static_cast<int>(vertex_idx);
|
||||
}
|
||||
its.indices.emplace_back(facet);
|
||||
}
|
||||
|
||||
*meshptr = TriangleMesh(std::move(its));
|
||||
|
||||
@@ -141,7 +141,7 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
|
||||
its.indices.emplace_back(indices[0], indices[1], indices[2]);
|
||||
int face_index =its.indices.size() - 1;
|
||||
RGBA face_color;
|
||||
auto set_face_color = [&uvs, &data, &mtl_data, &obj_info, &face_color](int face_index, const std::string mtl_name) {
|
||||
auto set_face_color = [&uvs, &data, &mtl_data, &obj_info, &face_color](int face_index, const std::string mtl_name, const std::array<int, 3> &corners) {
|
||||
if (mtl_data.new_mtl_unmap.find(mtl_name) != mtl_data.new_mtl_unmap.end()) {
|
||||
bool is_merge_ka_kd = true;
|
||||
for (size_t n = 0; n < 3; n++) {
|
||||
@@ -166,10 +166,15 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
|
||||
obj_info.uv_map_pngs[face_index] = png_name;
|
||||
}
|
||||
if (data.textureCoordinates.size() > 0) {
|
||||
Vec2f uv0(data.textureCoordinates[uvs[0] * 2], data.textureCoordinates[uvs[0] * 2 + 1]);
|
||||
Vec2f uv1(data.textureCoordinates[uvs[1] * 2], data.textureCoordinates[uvs[1] * 2 + 1]);
|
||||
Vec2f uv2(data.textureCoordinates[uvs[2] * 2], data.textureCoordinates[uvs[2] * 2 + 1]);
|
||||
std::array<Vec2f, 3> uv_array{uv0, uv1, uv2};
|
||||
// A face vertex may omit vt or reference a missing one. Fall back to (0, 0) rather than
|
||||
// skipping the face, so obj_info.uvs stays aligned with the face indices.
|
||||
const int uv_count = static_cast<int>(data.textureCoordinates.size() / OBJ_TEXCOORD_LENGTH);
|
||||
auto uv_at = [&data, uv_count](int idx) -> Vec2f {
|
||||
if (idx < 0 || idx >= uv_count)
|
||||
return Vec2f::Zero();
|
||||
return Vec2f(data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH], data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH + 1]);
|
||||
};
|
||||
std::array<Vec2f, 3> uv_array{uv_at(uvs[corners[0]]), uv_at(uvs[corners[1]]), uv_at(uvs[corners[2]])};
|
||||
obj_info.uvs.emplace_back(uv_array);
|
||||
}
|
||||
obj_info.face_colors.emplace_back(face_color);
|
||||
@@ -180,27 +185,27 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
|
||||
}
|
||||
}
|
||||
};
|
||||
auto set_face_color_by_mtl = [&data, &set_face_color](int face_index) {
|
||||
auto set_face_color_by_mtl = [&data, &set_face_color](int face_index, const std::array<int, 3> &corners) {
|
||||
if (data.usemtls.size() == 1) {
|
||||
set_face_color(face_index, data.usemtls[0].name);
|
||||
set_face_color(face_index, data.usemtls[0].name, corners);
|
||||
} else {
|
||||
for (size_t k = 0; k < data.usemtls.size(); k++) {
|
||||
auto mtl = data.usemtls[k];
|
||||
if (face_index >= mtl.face_start && face_index <= mtl.face_end) {
|
||||
set_face_color(face_index, data.usemtls[k].name);
|
||||
set_face_color(face_index, data.usemtls[k].name, corners);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (exist_mtl) {
|
||||
set_face_color_by_mtl(face_index);
|
||||
set_face_color_by_mtl(face_index, {0, 1, 2});
|
||||
}
|
||||
if (cnt == 4) {
|
||||
its.indices.emplace_back(indices[0], indices[2], indices[3]);
|
||||
int face_index = its.indices.size() - 1;
|
||||
if (exist_mtl) {
|
||||
set_face_color_by_mtl(face_index);
|
||||
set_face_color_by_mtl(face_index, {0, 2, 3});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,8 +217,12 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
|
||||
message = _L("This OBJ file couldn't be read because it's empty.");
|
||||
return false;
|
||||
}
|
||||
if (meshptr->volume() < 0)
|
||||
if (meshptr->volume() < 0) {
|
||||
meshptr->flip_triangles();
|
||||
// Flipping swaps corners 1 and 2 of every face, so the UVs have to follow.
|
||||
for (std::array<Vec2f, 3> &uv : obj_info.uvs)
|
||||
std::swap(uv[1], uv[2]);
|
||||
}
|
||||
// Hand the parsed material table back so callers can build a TexturedMesh from it.
|
||||
if (out_mtl)
|
||||
*out_mtl = mtl_data;
|
||||
|
||||
@@ -353,6 +353,10 @@ static constexpr const char* PART_TYPE = "part";
|
||||
static constexpr const char* NAME_KEY = "name";
|
||||
static constexpr const char* VOLUME_TYPE_KEY = "volume_type";
|
||||
static constexpr const char* PART_TYPE_KEY = "part_type";
|
||||
// Keep seam modes separate from the base type so older readers see a non-printing modifier.
|
||||
static constexpr const char* PRECISE_SEAM_TYPE_KEY = "precise_seam_type";
|
||||
// Preserve dormant settings without turning an older reader's modifier into an active override.
|
||||
static constexpr char PRECISE_SEAM_CONFIG_PREFIX[] = "precise_seam_config:";
|
||||
static constexpr const char* MATRIX_KEY = "matrix";
|
||||
static constexpr const char* SOURCE_FILE_KEY = "source_file";
|
||||
static constexpr const char* SOURCE_OBJECT_ID_KEY = "source_object_id";
|
||||
@@ -1629,7 +1633,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}
|
||||
while (it != m_plater_data.end())
|
||||
{
|
||||
if (it->first > m_plater_data.size())
|
||||
if (it->first <= 0 || static_cast<size_t>(it->first) > m_plater_data.size())
|
||||
{
|
||||
add_error("invalid plate index");
|
||||
return false;
|
||||
@@ -2312,7 +2316,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}
|
||||
while (it != m_plater_data.end())
|
||||
{
|
||||
if (it->first > m_plater_data.size())
|
||||
if (it->first <= 0 || static_cast<size_t>(it->first) > m_plater_data.size())
|
||||
{
|
||||
add_error("invalid plate index");
|
||||
return false;
|
||||
@@ -2508,19 +2512,26 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
XML_SetEntityDeclHandler(m_xml_parser, nullptr);
|
||||
XML_SetExternalEntityRefHandler(m_xml_parser, nullptr);
|
||||
|
||||
void* parser_buffer = XML_GetBuffer(m_xml_parser, (int)stat.m_uncomp_size);
|
||||
// expat sizes its buffer with an int, so a larger entry cannot be parsed in one piece.
|
||||
if (stat.m_uncomp_size > static_cast<mz_uint64>(std::numeric_limits<int>::max())) {
|
||||
add_error("Found invalid size");
|
||||
return false;
|
||||
}
|
||||
const int xml_size = static_cast<int>(stat.m_uncomp_size);
|
||||
|
||||
void* parser_buffer = XML_GetBuffer(m_xml_parser, xml_size);
|
||||
if (parser_buffer == nullptr) {
|
||||
add_error("Unable to create buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, (size_t)stat.m_uncomp_size, 0);
|
||||
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, static_cast<size_t>(xml_size), 0);
|
||||
if (res == 0) {
|
||||
add_error("Error while reading config data to buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!XML_ParseBuffer(m_xml_parser, (int)stat.m_uncomp_size, 1)) {
|
||||
if (!XML_ParseBuffer(m_xml_parser, xml_size, 1)) {
|
||||
char error_buf[1024];
|
||||
::snprintf(error_buf, 1024, "Error (%s) while parsing xml file at line %d", XML_ErrorString(XML_GetErrorCode(m_xml_parser)), (int)XML_GetCurrentLineNumber(m_xml_parser));
|
||||
add_error(error_buf);
|
||||
@@ -5212,6 +5223,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (auto &tc = volume_data->text_configuration; tc.has_value())
|
||||
volume->text_configuration = std::move(tc);
|
||||
|
||||
// Apply the seam mode after all base-type metadata, regardless of XML key order.
|
||||
ModelVolumeType precise_seam_type = ModelVolumeType::INVALID;
|
||||
// apply the remaining volume's metadata
|
||||
for (const Metadata& metadata : volume_data->metadata) {
|
||||
if (metadata.key == NAME_KEY)
|
||||
@@ -5221,6 +5234,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
//for old format
|
||||
else if ((metadata.key == VOLUME_TYPE_KEY) || (metadata.key == PART_TYPE_KEY))
|
||||
volume->set_type(ModelVolume::type_from_string(metadata.value));
|
||||
else if (metadata.key == PRECISE_SEAM_TYPE_KEY)
|
||||
precise_seam_type = ModelVolume::type_from_string(metadata.value);
|
||||
else if (boost::starts_with(metadata.key, PRECISE_SEAM_CONFIG_PREFIX))
|
||||
continue; // Restore dormant settings only after the final volume type is known.
|
||||
else if (metadata.key == SOURCE_FILE_KEY)
|
||||
volume->source.input_file = metadata.value;
|
||||
else if (metadata.key == SOURCE_OBJECT_ID_KEY)
|
||||
@@ -5243,6 +5260,22 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions);
|
||||
}
|
||||
|
||||
// Missing or unknown seam modes retain the ordinary modifier fallback.
|
||||
// Ignore seam metadata on other base types; legacy inline seam types still load above.
|
||||
if (volume->is_modifier() && is_precise_seam(precise_seam_type))
|
||||
volume->set_type(precise_seam_type);
|
||||
|
||||
// Unknown seam modes must remain inert modifiers, even when dormant settings are present.
|
||||
if (volume->is_precise_seam()) {
|
||||
for (const Metadata& metadata : volume_data->metadata) {
|
||||
if (boost::starts_with(metadata.key, PRECISE_SEAM_CONFIG_PREFIX)) {
|
||||
const std::string key = metadata.key.substr(sizeof(PRECISE_SEAM_CONFIG_PREFIX) - 1);
|
||||
if (!key.empty())
|
||||
volume->config.set_deserialize(key, metadata.value, config_substitutions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this may happen for 3mf saved by 3rd part softwares
|
||||
if (volume->name.empty()) {
|
||||
volume->name = object.name;
|
||||
@@ -8002,7 +8035,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
volume_id = m_volume_paths.find(volume)->second.second;
|
||||
stream << ID_ATTR << "=\"" << volume_id << "\" ";
|
||||
|
||||
stream << SUBTYPE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\">\n";
|
||||
// Older slicers must recognize the base type even when they ignore seam metadata.
|
||||
const ModelVolumeType stored_type = volume->is_precise_seam() ? ModelVolumeType::PARAMETER_MODIFIER : volume->type();
|
||||
stream << SUBTYPE_ATTR << "=\"" << ModelVolume::type_to_string(stored_type) << "\">\n";
|
||||
if (volume->is_precise_seam())
|
||||
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << PRECISE_SEAM_TYPE_KEY << "\" " << VALUE_ATTR << "=\"" <<
|
||||
ModelVolume::type_to_string(volume->type()) << "\"/>\n";
|
||||
//stream << " <" << PART_TAG << " " << ID_ATTR << "=\"" << it->second << "\" " << SUBTYPE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\">\n";
|
||||
|
||||
// stores volume's name
|
||||
@@ -8052,7 +8090,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
// stores volume's config data
|
||||
for (const std::string& key : volume->config.keys()) {
|
||||
stream << " <" << METADATA_TAG << " "<< KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n";
|
||||
// Seam settings are inactive but must survive changing the helper back into a part/modifier.
|
||||
const bool dormant = volume->is_precise_seam();
|
||||
const std::string stored_key = dormant ? PRECISE_SEAM_CONFIG_PREFIX + key : key;
|
||||
const std::string value = volume->config.opt_serialize(key);
|
||||
// Config serialization is C-style, not XML: escape active settings too, including tabs.
|
||||
stream << " <" << METADATA_TAG << " "<< KEY_ATTR << "=\"" << stored_key << "\" " << VALUE_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(value) << "\"/>\n";
|
||||
}
|
||||
|
||||
if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) {
|
||||
|
||||
@@ -51,19 +51,18 @@ static bool obj_parseline(const char *line, ObjData &data)
|
||||
line = endptr;
|
||||
EATWS();
|
||||
}
|
||||
/*double w = 0;
|
||||
// The optional w is accepted but not stored: only u and v are used.
|
||||
if (*line != 0) {
|
||||
w = strtod(line, &endptr);
|
||||
strtod(line, &endptr);
|
||||
if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0))
|
||||
return false;
|
||||
line = endptr;
|
||||
EATWS();
|
||||
}*/
|
||||
}
|
||||
if (*line != 0)
|
||||
return false;
|
||||
data.textureCoordinates.push_back((float)u);
|
||||
data.textureCoordinates.push_back((float)v);
|
||||
//data.textureCoordinates.push_back((float)w);
|
||||
break;
|
||||
}
|
||||
case 'n':
|
||||
@@ -245,7 +244,7 @@ static bool obj_parseline(const char *line, ObjData &data)
|
||||
else
|
||||
-- vertex.normalIdx;
|
||||
if (vertex.textureCoordIdx < 0)
|
||||
vertex.textureCoordIdx += (int)data.textureCoordinates.size() / 3;
|
||||
vertex.textureCoordIdx += (int)data.textureCoordinates.size() / OBJ_TEXCOORD_LENGTH;
|
||||
else
|
||||
-- vertex.textureCoordIdx;
|
||||
data.vertices.push_back(vertex);
|
||||
|
||||
@@ -92,6 +92,7 @@ inline bool operator==(const ObjSmoothingGroup &v1, const ObjSmoothingGroup &v2)
|
||||
}
|
||||
#define OBJ_VERTEX_COLOR_ALPHA 6
|
||||
#define OBJ_VERTEX_LENGTH 7 // x, y, z, color_x,color_y,color_z,color_w
|
||||
#define OBJ_TEXCOORD_LENGTH 2 // u, v
|
||||
#define ONE_FACE_SIZE 4//ONE_FACE format: f 8/4/6 7/3/6 6/2/6 -1/-1/-1
|
||||
struct ObjData {
|
||||
// Version of the data structure for load / store in the private binary format.
|
||||
@@ -100,7 +101,7 @@ struct ObjData {
|
||||
// x, y, z, color_x,color_y,color_z,color_w
|
||||
std::vector<float> coordinates;
|
||||
bool has_vertex_color{false};
|
||||
// u, v, w
|
||||
// u, v
|
||||
std::vector<float> textureCoordinates;
|
||||
// x, y, z
|
||||
std::vector<float> normals;
|
||||
|
||||
+72
-89
@@ -1497,12 +1497,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
check_add_eol(toolchange_gcode_str);
|
||||
|
||||
// SoftFever: set new PA for new filament
|
||||
if (gcodegen.config().enable_pressure_advance.get_at(new_filament_id)) {
|
||||
gcode += gcodegen.writer().set_pressure_advance(gcodegen.config().pressure_advance.get_at(new_filament_id));
|
||||
// Orca: Adaptive PA
|
||||
// Reset Adaptive PA processor last PA value
|
||||
gcodegen.m_pa_processor->resetPreviousPA(gcodegen.config().pressure_advance.get_at(new_filament_id));
|
||||
}
|
||||
gcode += gcodegen.set_filament_pressure_advance(new_filament_id);
|
||||
|
||||
// A phony move to the end position at the wipe tower.
|
||||
gcodegen.writer().travel_to_xy((end_pos + plate_origin_2d).cast<double>());
|
||||
@@ -1826,12 +1821,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
check_add_eol(toolchange_gcode_str);
|
||||
|
||||
// SoftFever: set new PA for new filament
|
||||
if (new_extruder_id != -1 && gcodegen.config().enable_pressure_advance.get_at(new_extruder_id)) {
|
||||
gcode += gcodegen.writer().set_pressure_advance(gcodegen.config().pressure_advance.get_at(new_extruder_id));
|
||||
// Orca: Adaptive PA
|
||||
// Reset Adaptive PA processor last PA value
|
||||
gcodegen.m_pa_processor->resetPreviousPA(gcodegen.config().pressure_advance.get_at(new_extruder_id));
|
||||
}
|
||||
if (new_extruder_id != -1)
|
||||
gcode += gcodegen.set_filament_pressure_advance(new_extruder_id);
|
||||
|
||||
// A phony move to the end position at the wipe tower.
|
||||
gcodegen.writer().travel_to_xy((end_pos + plate_origin_2d).cast<double>());
|
||||
@@ -3232,7 +3223,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
m_cooling_buffer->set_current_extruder(initial_extruder_id, extruder_id);
|
||||
|
||||
// Orca: Initialise AdaptivePA processor filter
|
||||
m_pa_processor = std::make_unique<AdaptivePAProcessor>(*this, tool_ordering.all_extruders());
|
||||
m_pa_processor = std::make_unique<AdaptivePAProcessor>(*this);
|
||||
|
||||
// Update output variables after the extruders were initialized.
|
||||
m_placeholder_parser_integration.init(m_writer);
|
||||
@@ -3797,12 +3788,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
}
|
||||
}
|
||||
// Orca: add missing PA settings for initial filament
|
||||
if (m_config.enable_pressure_advance.get_at(initial_non_support_extruder_id)) {
|
||||
file.write(m_writer.set_pressure_advance(m_config.pressure_advance.get_at(initial_non_support_extruder_id)));
|
||||
// Orca: Adaptive PA
|
||||
// Reset Adaptive PA processor last PA value
|
||||
m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(initial_non_support_extruder_id));
|
||||
}
|
||||
file.write(set_filament_pressure_advance(initial_non_support_extruder_id));
|
||||
}
|
||||
|
||||
//flush FanMover buffer to avoid modifying the start gcode if it's manual.
|
||||
@@ -4188,6 +4174,28 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
|
||||
print.throw_if_canceled();
|
||||
|
||||
// Some firmwares only scan the last N lines for the time estimate, so the stats are written
|
||||
// after the config block.
|
||||
if (!is_bbl_printers && !skip_config_block) {
|
||||
file.write("; CONFIG_BLOCK_START\n");
|
||||
std::string full_config;
|
||||
append_full_config(print, full_config);
|
||||
if (!full_config.empty())
|
||||
file.write(full_config);
|
||||
|
||||
// SoftFever: write compatiple info
|
||||
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature);
|
||||
file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str());
|
||||
file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0));
|
||||
file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value);
|
||||
|
||||
//SF TODO
|
||||
// file.write_format("; variable_layer_height = %d\n", print.ad.adaptive_layer_height ? 1 : 0);
|
||||
|
||||
file.write("; CONFIG_BLOCK_END\n\n");
|
||||
}
|
||||
|
||||
// Get filament stats.
|
||||
file.write(DoExport::update_print_stats_and_format_filament_stats(
|
||||
// Const inputs
|
||||
@@ -4198,6 +4206,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
// Const input (tool-change fallback for non-wipe-tower prints)
|
||||
print.tool_ordering()));
|
||||
print.m_print_statistics.initial_tool = initial_extruder_id;
|
||||
print.m_print_statistics.initial_no_support_tool = initial_non_support_extruder_id;
|
||||
if (!is_bbl_printers) {
|
||||
file.write_format("; total filament used [g] = %.2lf\n",
|
||||
print.m_print_statistics.total_weight);
|
||||
@@ -4213,26 +4222,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder)
|
||||
.c_str());
|
||||
file.write("\n");
|
||||
if (!skip_config_block) {
|
||||
file.write("; CONFIG_BLOCK_START\n");
|
||||
std::string full_config;
|
||||
append_full_config(print, full_config);
|
||||
if (!full_config.empty())
|
||||
file.write(full_config);
|
||||
|
||||
// SoftFever: write compatiple info
|
||||
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature);
|
||||
file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str());
|
||||
file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0));
|
||||
file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value);
|
||||
|
||||
//SF TODO
|
||||
// file.write_format("; variable_layer_height = %d\n", print.ad.adaptive_layer_height ? 1 : 0);
|
||||
|
||||
file.write("; CONFIG_BLOCK_END\n\n");
|
||||
} // !skip_config_block
|
||||
|
||||
}
|
||||
file.write("\n");
|
||||
|
||||
@@ -8389,12 +8378,16 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
|
||||
// Orca: Dynamic PA
|
||||
// If adaptive PA is enabled, by default evaluate PA on all extrusion moves
|
||||
// The PA settings and the PA_CHANGE tags use the filament config index of the filament's extruder variant
|
||||
const unsigned int pa_config_index = (unsigned int) get_filament_config_index(m_writer.filament()->id());
|
||||
const bool adaptive_pa = m_config.adaptive_pressure_advance.get_at(pa_config_index) && m_config.enable_pressure_advance.get_at(pa_config_index);
|
||||
const bool adaptive_pa_overhangs = adaptive_pa && m_config.adaptive_pressure_advance_overhangs.get_at(pa_config_index);
|
||||
bool is_pa_calib = m_curr_print->calib_mode() == CalibMode::Calib_PA_Line ||
|
||||
m_curr_print->calib_mode() == CalibMode::Calib_PA_Pattern ||
|
||||
m_curr_print->calib_mode() == CalibMode::Calib_PA_Tower;
|
||||
bool evaluate_adaptive_pa = false;
|
||||
bool role_change = (m_last_extrusion_role != path.role());
|
||||
if (!is_pa_calib && FILAMENT_CONFIG(adaptive_pressure_advance) && FILAMENT_CONFIG(enable_pressure_advance)) {
|
||||
if (!is_pa_calib && adaptive_pa) {
|
||||
evaluate_adaptive_pa = true;
|
||||
// If we have already emmited a PA change because the m_multi_flow_segment_path_pa_set is set
|
||||
// skip re-issuing the PA change tag.
|
||||
@@ -8477,7 +8470,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
// Post processor flag generation code segment when option to emit only at role changes is enabled
|
||||
// Variables published to the post processor:
|
||||
// 1) Tag to trigger a PA evaluation (because a role change was identified and the user has requested dynamic PA adjustments)
|
||||
// 2) Current extruder ID (to identify the PA model for the currently used extruder)
|
||||
// 2) Current filament config index (to identify the PA model for the currently used filament and its extruder variant)
|
||||
// 3) mm3_per_mm value (to then multiply by the final model print speed after slowdown for cooling is applied)
|
||||
// 4) the current acceleration (to pass to the model for evaluation)
|
||||
// 5) whether this is an external perimeter (for future use)
|
||||
@@ -8488,7 +8481,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
if (m_multi_flow_segment_path_average_mm3_per_mm > 0) {
|
||||
sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n",
|
||||
GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(),
|
||||
m_writer.filament()->id(),
|
||||
pa_config_index,
|
||||
m_multi_flow_segment_path_average_mm3_per_mm,
|
||||
acceleration_i,
|
||||
((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)),
|
||||
@@ -8501,7 +8494,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
// to issue a zero flow PA change command for this
|
||||
sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n",
|
||||
GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(),
|
||||
m_writer.filament()->id(),
|
||||
pa_config_index,
|
||||
_mm3_per_mm,
|
||||
acceleration_i,
|
||||
((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)),
|
||||
@@ -8609,11 +8602,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
// or a flow change, so emit the flag to evaluate PA for the upcomming extrusion
|
||||
// Emit tag before new speed is set so the post processor reads the next speed immediately and uses it.
|
||||
// Dont emit tag if it has just already been emitted from a role change above
|
||||
if(_mm3_per_mm >0 &&
|
||||
FILAMENT_CONFIG(adaptive_pressure_advance) &&
|
||||
FILAMENT_CONFIG(enable_pressure_advance) &&
|
||||
FILAMENT_CONFIG(adaptive_pressure_advance_overhangs) &&
|
||||
!evaluate_adaptive_pa){
|
||||
if(_mm3_per_mm >0 && adaptive_pa_overhangs && !evaluate_adaptive_pa){
|
||||
if(writer().get_current_speed() > F){ // Ramping down speed - use overhang logic where the minimum speed is used between current and upcoming extrusion
|
||||
if(m_config.gcode_comments){
|
||||
sprintf(buf, "; Ramp down-non-variable\n");
|
||||
@@ -8621,7 +8610,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
}
|
||||
sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n",
|
||||
GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(),
|
||||
m_writer.filament()->id(),
|
||||
pa_config_index,
|
||||
_mm3_per_mm,
|
||||
acceleration_i,
|
||||
((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)),
|
||||
@@ -8636,7 +8625,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
}
|
||||
sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n",
|
||||
GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(),
|
||||
m_writer.filament()->id(),
|
||||
pa_config_index,
|
||||
_mm3_per_mm,
|
||||
acceleration_i,
|
||||
((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)),
|
||||
@@ -8842,10 +8831,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
// ORCA: Adaptive PA code segment when adjusting PA within the same feature
|
||||
// There is a speed change or flow change so emit the flag to evaluate PA for the upcomming extrusion
|
||||
// Emit tag before new speed is set so the post processor reads the next speed immediately and uses it.
|
||||
if(_mm3_per_mm >0 &&
|
||||
EXTRUDER_CONFIG(adaptive_pressure_advance) &&
|
||||
EXTRUDER_CONFIG(enable_pressure_advance) &&
|
||||
EXTRUDER_CONFIG(adaptive_pressure_advance_overhangs) ){
|
||||
if(_mm3_per_mm >0 && adaptive_pa_overhangs){
|
||||
if(last_set_speed > new_speed){ // Ramping down speed - use overhang logic where the minimum speed is used between current and upcoming extrusion
|
||||
if(m_config.gcode_comments) {
|
||||
sprintf(buf, "; Ramp up-variable\n");
|
||||
@@ -8853,7 +8839,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
}
|
||||
sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n",
|
||||
GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(),
|
||||
m_writer.filament()->id(),
|
||||
pa_config_index,
|
||||
_mm3_per_mm,
|
||||
acceleration_i,
|
||||
((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)),
|
||||
@@ -8868,7 +8854,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
}
|
||||
sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n",
|
||||
GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(),
|
||||
m_writer.filament()->id(),
|
||||
pa_config_index,
|
||||
_mm3_per_mm,
|
||||
acceleration_i,
|
||||
((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)),
|
||||
@@ -9421,33 +9407,29 @@ void GCode::update_placeholder_parser_with_variant_params()
|
||||
if (num_filaments == 0)
|
||||
return;
|
||||
|
||||
// Helpers: remap config arrays from variant index space to filament_id index space.
|
||||
// Helper: remap config arrays from variant index space to filament_id index space.
|
||||
// After remapping, gcode templates can use param[filament_id] directly.
|
||||
auto remap_floats_by_filament = [&](const auto &src) {
|
||||
std::vector<double> dst(num_filaments);
|
||||
for (size_t i = 0; i < num_filaments; ++i)
|
||||
dst[i] = src.get_at(get_filament_config_index(i));
|
||||
return dst;
|
||||
};
|
||||
auto remap_ints_by_filament = [&](const auto &src) {
|
||||
std::vector<int> dst(num_filaments);
|
||||
auto remap_by_filament = [&](const auto &src) {
|
||||
std::decay_t<decltype(src.values)> dst(num_filaments);
|
||||
for (size_t i = 0; i < num_filaments; ++i)
|
||||
dst[i] = src.get_at(get_filament_config_index(i));
|
||||
return dst;
|
||||
};
|
||||
|
||||
// --- filament_options_with_variant: gcode indexes by filament_id ---
|
||||
this->placeholder_parser().set("filament_max_volumetric_speed", new ConfigOptionFloats(remap_floats_by_filament(m_config.filament_max_volumetric_speed)));
|
||||
this->placeholder_parser().set("filament_pre_cooling_temperature", new ConfigOptionInts(remap_ints_by_filament(m_config.filament_pre_cooling_temperature)));
|
||||
this->placeholder_parser().set("filament_pre_cooling_temperature_nc", new ConfigOptionInts(remap_ints_by_filament(m_config.filament_pre_cooling_temperature_nc)));
|
||||
this->placeholder_parser().set("filament_cooling_before_tower", new ConfigOptionFloats(remap_floats_by_filament(m_config.filament_cooling_before_tower)));
|
||||
this->placeholder_parser().set("nozzle_temperature_initial_layer", new ConfigOptionInts(remap_ints_by_filament(m_config.nozzle_temperature_initial_layer)));
|
||||
this->placeholder_parser().set("nozzle_temperature", new ConfigOptionInts(remap_ints_by_filament(m_config.nozzle_temperature)));
|
||||
this->placeholder_parser().set("filament_max_volumetric_speed", new ConfigOptionFloats(remap_by_filament(m_config.filament_max_volumetric_speed)));
|
||||
this->placeholder_parser().set("filament_pre_cooling_temperature", new ConfigOptionInts(remap_by_filament(m_config.filament_pre_cooling_temperature)));
|
||||
this->placeholder_parser().set("filament_pre_cooling_temperature_nc", new ConfigOptionInts(remap_by_filament(m_config.filament_pre_cooling_temperature_nc)));
|
||||
this->placeholder_parser().set("filament_cooling_before_tower", new ConfigOptionFloats(remap_by_filament(m_config.filament_cooling_before_tower)));
|
||||
this->placeholder_parser().set("nozzle_temperature_initial_layer", new ConfigOptionInts(remap_by_filament(m_config.nozzle_temperature_initial_layer)));
|
||||
this->placeholder_parser().set("nozzle_temperature", new ConfigOptionInts(remap_by_filament(m_config.nozzle_temperature)));
|
||||
// first_layer_temperature is a legacy alias of nozzle_temperature_initial_layer
|
||||
this->placeholder_parser().set("first_layer_temperature", new ConfigOptionInts(remap_ints_by_filament(m_config.nozzle_temperature_initial_layer)));
|
||||
this->placeholder_parser().set("first_layer_temperature", new ConfigOptionInts(remap_by_filament(m_config.nozzle_temperature_initial_layer)));
|
||||
this->placeholder_parser().set("pressure_advance", new ConfigOptionFloats(remap_by_filament(m_config.pressure_advance)));
|
||||
this->placeholder_parser().set("enable_pressure_advance", new ConfigOptionBools(remap_by_filament(m_config.enable_pressure_advance)));
|
||||
|
||||
// --- printer_options_with_variant_1: in m_config these are already merged as filament-indexed ---
|
||||
this->placeholder_parser().set("retraction_distances_when_cut", new ConfigOptionFloats(remap_floats_by_filament(m_config.retraction_distances_when_cut)));
|
||||
this->placeholder_parser().set("retraction_distances_when_cut", new ConfigOptionFloats(remap_by_filament(m_config.retraction_distances_when_cut)));
|
||||
// hotend_cooling_rate / hotend_heating_rate: gcode uses [filament_map[x]-1] (extruder_id), no remap needed
|
||||
|
||||
// --- filament_map: per-layer dynamic, sync from m_config to placeholder_parser ---
|
||||
@@ -9457,10 +9439,10 @@ void GCode::update_placeholder_parser_with_variant_params()
|
||||
{
|
||||
// Fast purge mode uses filament_flush_temp_fast; Default is inert.
|
||||
bool use_fast_flush = m_config.prime_volume_mode == PrimeVolumeMode::pvmFast;
|
||||
auto flush_v_speed = remap_floats_by_filament(m_config.filament_flush_volumetric_speed);
|
||||
auto filament_max_v = remap_floats_by_filament(m_config.filament_max_volumetric_speed);
|
||||
auto flush_temps = remap_ints_by_filament(use_fast_flush ? m_config.filament_flush_temp_fast
|
||||
: m_config.filament_flush_temp);
|
||||
auto flush_v_speed = remap_by_filament(m_config.filament_flush_volumetric_speed);
|
||||
auto filament_max_v = remap_by_filament(m_config.filament_max_volumetric_speed);
|
||||
auto flush_temps = remap_by_filament(use_fast_flush ? m_config.filament_flush_temp_fast
|
||||
: m_config.filament_flush_temp);
|
||||
for (size_t i = 0; i < num_filaments; ++i) {
|
||||
if (flush_v_speed[i] == 0)
|
||||
flush_v_speed[i] = filament_max_v[i];
|
||||
@@ -9512,12 +9494,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
|
||||
gcode += this->placeholder_parser_process("filament_start_gcode", filament_start_gcode, new_filament_id, &config);
|
||||
check_add_eol(gcode);
|
||||
}
|
||||
if (m_config.enable_pressure_advance.get_at(new_filament_id)) {
|
||||
gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id));
|
||||
// Orca: Adaptive PA
|
||||
// Reset Adaptive PA processor last PA value
|
||||
m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id));
|
||||
}
|
||||
gcode += set_filament_pressure_advance(new_filament_id);
|
||||
|
||||
gcode += m_writer.toolchange(new_filament_id, new_extruder_id);
|
||||
if (Extruder *fil = m_writer.filament())
|
||||
@@ -9908,18 +9885,24 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
|
||||
if (m_ooze_prevention.enable && !defer_temp_wait)
|
||||
gcode += m_ooze_prevention.post_toolchange(*this);
|
||||
|
||||
if (m_config.enable_pressure_advance.get_at(new_filament_id)) {
|
||||
gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id));
|
||||
// Orca: Adaptive PA
|
||||
// Reset Adaptive PA processor last PA value
|
||||
m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id));
|
||||
}
|
||||
gcode += set_filament_pressure_advance(new_filament_id);
|
||||
//Orca: tool changer or IDEX's firmware may change Z position, so we set it to unknown/undefined
|
||||
m_last_pos_defined = false;
|
||||
|
||||
return gcode;
|
||||
}
|
||||
|
||||
std::string GCode::set_filament_pressure_advance(unsigned int filament_id)
|
||||
{
|
||||
const size_t fi = get_filament_config_index(filament_id);
|
||||
if (!m_config.enable_pressure_advance.get_at(fi))
|
||||
return {};
|
||||
// Orca: Adaptive PA
|
||||
// Reset Adaptive PA processor last PA value
|
||||
m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(fi));
|
||||
return m_writer.set_pressure_advance(m_config.pressure_advance.get_at(fi));
|
||||
}
|
||||
|
||||
inline std::string polygon_to_string(const Polygon &polygon, Print *print, bool is_print_space = false) {
|
||||
std::ostringstream gcode;
|
||||
gcode << "[";
|
||||
|
||||
@@ -281,6 +281,8 @@ public:
|
||||
// extra_retract forwards a PETG pre-extrusion over-extrusion; default 0 -> identical to the plain deretract.
|
||||
std::string unretract(float extra_retract = 0.f) { return m_writer.unlift() + m_writer.unretract(extra_retract); }
|
||||
std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1, bool defer_temp_wait = false);
|
||||
// Sets the pressure advance of the filament's extruder variant, if enabled for it.
|
||||
std::string set_filament_pressure_advance(unsigned int filament_id);
|
||||
bool is_BBL_Printer();
|
||||
WipeTowerType wipe_tower_type();
|
||||
|
||||
|
||||
@@ -21,38 +21,30 @@ namespace Slic3r {
|
||||
*
|
||||
* @param gcodegen A reference to the GCode object that generates the G-code.
|
||||
*/
|
||||
AdaptivePAProcessor::AdaptivePAProcessor(GCode &gcodegen, const std::vector<unsigned int> &tools_used)
|
||||
AdaptivePAProcessor::AdaptivePAProcessor(GCode &gcodegen)
|
||||
: m_gcodegen(gcodegen),
|
||||
m_config(gcodegen.config()),
|
||||
m_last_predicted_pa(0.0),
|
||||
m_max_next_feedrate(0.0),
|
||||
m_next_feedrate(0.0),
|
||||
m_current_feedrate(0.0),
|
||||
m_last_extruder_id(-1),
|
||||
m_last_config_index(-1),
|
||||
m_pa_change_pattern(R"(; PA_CHANGE:T(\d+) MM3MM:([0-9]*\.[0-9]+) ACCEL:(\d+) BR:(\d+) RC:(\d+) OV:(\d+))"),
|
||||
m_g1_f_pattern(R"(G1 F([0-9]+))")
|
||||
{
|
||||
// Constructor body can be used for further initialization if necessary
|
||||
for (unsigned int tool : tools_used) {
|
||||
// Only enable model for the tool if both PA and adaptive PA options are enabled
|
||||
if(m_config.adaptive_pressure_advance.get_at(tool) && m_config.enable_pressure_advance.get_at(tool)){
|
||||
auto interpolator = std::make_unique<AdaptivePAInterpolator>();
|
||||
// Get calibration values from extruder
|
||||
std::string pa_calibration_values = m_config.adaptive_pressure_advance_model.get_at(tool);
|
||||
// Setup the model and store it in the tool-interpolation model map
|
||||
interpolator->parseAndSetData(pa_calibration_values);
|
||||
m_AdaptivePAInterpolators[tool] = std::move(interpolator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method to get the interpolator for a specific tool ID
|
||||
AdaptivePAInterpolator* AdaptivePAProcessor::getInterpolator(unsigned int tool_id) {
|
||||
auto it = m_AdaptivePAInterpolators.find(tool_id);
|
||||
if (it != m_AdaptivePAInterpolators.end()) {
|
||||
return it->second.get();
|
||||
// Method to get the interpolator for a specific filament config index.
|
||||
// The model is built the first time an index is requested, as the indices in use depend on
|
||||
// the extruder variant each filament prints with.
|
||||
AdaptivePAInterpolator* AdaptivePAProcessor::getInterpolator(unsigned int config_index) {
|
||||
auto [it, inserted] = m_AdaptivePAInterpolators.try_emplace(config_index);
|
||||
// Only enable model for the index if both PA and adaptive PA options are enabled
|
||||
if (inserted && m_config.adaptive_pressure_advance.get_at(config_index) && m_config.enable_pressure_advance.get_at(config_index)) {
|
||||
it->second = std::make_unique<AdaptivePAInterpolator>();
|
||||
it->second->parseAndSetData(m_config.adaptive_pressure_advance_model.get_at(config_index));
|
||||
}
|
||||
return nullptr; // Handle the case where the tool_id was not found
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,16 +100,17 @@ std::string AdaptivePAProcessor::process_layer(std::string &&gcode) {
|
||||
// the PA for that material is set. As no tag below will be found for this extruder, the original PA is retained.
|
||||
if (line.find("; PA_CHANGE") == 0) { // prune lines quickly before running regex check as regex is more expensive to run
|
||||
if (std::regex_search(line, m_match, m_pa_change_pattern)) {
|
||||
int extruder_id = std::stoi(m_match[1].str());
|
||||
// The tag carries the filament config index, which selects the PA settings of the filament's extruder variant
|
||||
int config_index = std::stoi(m_match[1].str());
|
||||
mm3mm_value = std::stod(m_match[2].str());
|
||||
accel_value = std::stod(m_match[3].str());
|
||||
int isBridge = std::stoi(m_match[4].str());
|
||||
int roleChange = std::stoi(m_match[5].str());
|
||||
int isOverhang = std::stoi(m_match[6].str());
|
||||
|
||||
// Check if the extruder ID has changed
|
||||
bool extruder_changed = (extruder_id != m_last_extruder_id);
|
||||
m_last_extruder_id = extruder_id;
|
||||
// Check if the filament config index has changed
|
||||
bool config_index_changed = (config_index != m_last_config_index);
|
||||
m_last_config_index = config_index;
|
||||
|
||||
// Save the PA_CHANGE line to output later after finding feedrate
|
||||
pa_change_line = line;
|
||||
@@ -212,23 +205,23 @@ std::string AdaptivePAProcessor::process_layer(std::string &&gcode) {
|
||||
|
||||
// Calculate the predicted PA using the upcomming feature maximum feedrate
|
||||
// Get the interpolator for the active tool
|
||||
AdaptivePAInterpolator* interpolator = getInterpolator(m_last_extruder_id);
|
||||
AdaptivePAInterpolator* interpolator = getInterpolator(m_last_config_index);
|
||||
|
||||
double predicted_pa = 0;
|
||||
double adaptive_PA_speed = 0;
|
||||
|
||||
if(!interpolator){ // Tool not found in the interpolator map
|
||||
// Tool not found in the PA interpolator to tool map
|
||||
predicted_pa = m_config.enable_pressure_advance.get_at(m_last_extruder_id) ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0;
|
||||
predicted_pa = m_config.enable_pressure_advance.get_at(m_last_config_index) ? m_config.pressure_advance.get_at(m_last_config_index) : 0;
|
||||
if(m_config.gcode_comments) output << "; APA: Tool doesnt have APA enabled\n";
|
||||
} else if (!interpolator->isInitialised() || (!m_config.adaptive_pressure_advance.get_at(m_last_extruder_id)) )
|
||||
} else if (!interpolator->isInitialised() || (!m_config.adaptive_pressure_advance.get_at(m_last_config_index)) )
|
||||
// Check if the model is not initialised by the constructor for the active extruder
|
||||
// Also check that adaptive PA is enabled for that extruder. This should not be needed
|
||||
// as the PA change flag should not be set upstream (in the GCode.cpp file) if adaptive PA is disabled
|
||||
// however check for robustness sake.
|
||||
{
|
||||
// Model failed or adaptive pressure advance not enabled - use default value from m_config
|
||||
predicted_pa = m_config.enable_pressure_advance.get_at(m_last_extruder_id) ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0;
|
||||
predicted_pa = m_config.enable_pressure_advance.get_at(m_last_config_index) ? m_config.pressure_advance.get_at(m_last_config_index) : 0;
|
||||
if(m_config.gcode_comments) output << "; APA: Interpolator setup failed, using default pressure advance\n";
|
||||
} else { // Model setup succeeded
|
||||
// Proceed to identify the print speed to use to calculate the adaptive PA value
|
||||
@@ -249,18 +242,18 @@ std::string AdaptivePAProcessor::process_layer(std::string &&gcode) {
|
||||
predicted_pa = (*interpolator)(mm3mm_value * adaptive_PA_speed, accel_value);
|
||||
|
||||
// This is a bridge, use the dedicated PA setting.
|
||||
if(isBridge && m_config.adaptive_pressure_advance_bridges.get_at(m_last_extruder_id) > EPSILON)
|
||||
predicted_pa = m_config.adaptive_pressure_advance_bridges.get_at(m_last_extruder_id);
|
||||
if(isBridge && m_config.adaptive_pressure_advance_bridges.get_at(m_last_config_index) > EPSILON)
|
||||
predicted_pa = m_config.adaptive_pressure_advance_bridges.get_at(m_last_config_index);
|
||||
|
||||
if (predicted_pa < 0) { // If extrapolation fails, fall back to the default PA for the extruder.
|
||||
predicted_pa = m_config.enable_pressure_advance.get_at(m_last_extruder_id) ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0;
|
||||
predicted_pa = m_config.enable_pressure_advance.get_at(m_last_config_index) ? m_config.pressure_advance.get_at(m_last_config_index) : 0;
|
||||
if(m_config.gcode_comments) output << "; APA: Interpolation failed, using fallback pressure advance value\n";
|
||||
}
|
||||
}
|
||||
if(m_config.gcode_comments) {
|
||||
// Output debug GCode comments
|
||||
output << pa_change_line << '\n'; // Output PA change command tag
|
||||
if(isBridge && m_config.adaptive_pressure_advance_bridges.get_at(m_last_extruder_id) > EPSILON)
|
||||
if(isBridge && m_config.adaptive_pressure_advance_bridges.get_at(m_last_config_index) > EPSILON)
|
||||
output << "; APA Model Override (bridge)\n";
|
||||
output << "; APA Current Speed: " << std::to_string(m_current_feedrate) << "\n";
|
||||
output << "; APA Next Speed: " << std::to_string(m_next_feedrate) << "\n";
|
||||
@@ -269,7 +262,7 @@ std::string AdaptivePAProcessor::process_layer(std::string &&gcode) {
|
||||
output << "; APA Flow rate: " << std::to_string(mm3mm_value * m_max_next_feedrate) << "\n";
|
||||
output << "; APA Prev PA: " << std::to_string(m_last_predicted_pa) << " New PA: " << std::to_string(predicted_pa) << "\n";
|
||||
}
|
||||
if (extruder_changed || std::fabs(predicted_pa - m_last_predicted_pa) > EPSILON) {
|
||||
if (config_index_changed || std::fabs(predicted_pa - m_last_predicted_pa) > EPSILON) {
|
||||
output << m_gcodegen.writer().set_pressure_advance(predicted_pa); // Use m_writer to set pressure advance
|
||||
m_last_predicted_pa = predicted_pa; // Update the last predicted PA value
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <regex>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include "AdaptivePAInterpolator.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -33,7 +32,7 @@ public:
|
||||
*
|
||||
* @param gcodegen A reference to the GCode object that generates the G-code.
|
||||
*/
|
||||
AdaptivePAProcessor(GCode &gcodegen, const std::vector<unsigned int> &tools_used);
|
||||
AdaptivePAProcessor(GCode &gcodegen);
|
||||
|
||||
/**
|
||||
* @brief Processes a layer of G-code and applies adaptive pressure advance.
|
||||
@@ -70,28 +69,25 @@ public:
|
||||
|
||||
private:
|
||||
GCode &m_gcodegen; ///< Reference to the GCode object.
|
||||
std::unordered_map<unsigned int, std::unique_ptr<AdaptivePAInterpolator>> m_AdaptivePAInterpolators; ///< Map between Interpolator objects and tool ID's
|
||||
std::unordered_map<unsigned int, std::unique_ptr<AdaptivePAInterpolator>> m_AdaptivePAInterpolators; ///< Map between Interpolator objects and filament config indices (null when adaptive PA is off)
|
||||
const PrintConfig &m_config; ///< Reference to the print configuration.
|
||||
double m_last_predicted_pa; ///< Last predicted pressure advance value.
|
||||
double m_max_next_feedrate; ///< Maximum feed rate (speed) for the upcomming island. If no speed is found, the previous island speed is used.
|
||||
double m_next_feedrate; ///< First feed rate (speed) for the upcomming island.
|
||||
double m_current_feedrate; ///< Current, latest feedrate.
|
||||
int m_last_extruder_id; ///< Last used extruder ID.
|
||||
int m_last_config_index; ///< Filament config index of the last PA_CHANGE tag.
|
||||
|
||||
std::regex m_pa_change_pattern; ///< Regular expression to detect PA_CHANGE pattern.
|
||||
std::regex m_g1_f_pattern; ///< Regular expression to detect G1 F pattern.
|
||||
std::smatch m_match; ///< Match results for regular expressions.
|
||||
|
||||
/**
|
||||
* @brief Get the PA interpolator attached to the specified tool ID.
|
||||
* @brief Get the PA interpolator attached to the specified filament config index.
|
||||
*
|
||||
* This method manually sets the adaptive PA internally held value.
|
||||
* Call this when changing tools or in any other case where the internally assumed last PA value may be incorrect
|
||||
*
|
||||
* @param An integer with the tool ID for which the PA interpolation model is to be returned.
|
||||
* @return The Adaptive PA Interpolator object corresponding to that tool.
|
||||
* @param config_index The filament config index (one per filament and extruder variant) for which the PA interpolation model is to be returned.
|
||||
* @return The Adaptive PA Interpolator object corresponding to that index, or nullptr when adaptive PA is off for it.
|
||||
*/
|
||||
AdaptivePAInterpolator* getInterpolator(unsigned int tool_id);
|
||||
AdaptivePAInterpolator* getInterpolator(unsigned int config_index);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,146 @@
|
||||
#ifndef slic3r_PreciseSeam_hpp_
|
||||
#define slic3r_PreciseSeam_hpp_
|
||||
|
||||
#include <atomic>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Layer.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "SeamPlacer.hpp"
|
||||
|
||||
// CURRENT STATUS:
|
||||
// Strong modifiers (Center/Left/Right): only one intersection per perimeter is supported,
|
||||
// since there can be only one seam. Additional intersections are ignored.
|
||||
//
|
||||
// Weak modifiers (Enforced/Blocked/Neutral): multiple intersections are supported,
|
||||
// but none of them should pass through the model entirely. A through-body intersection
|
||||
// produces multiple segments, of which only one will be processed.
|
||||
//
|
||||
// In both cases, a pop-up warning is shown when unsupported intersections are detected.
|
||||
//
|
||||
// If any modifier has a multiply-connected cross-section (e.g. a hollow shape),
|
||||
// it is skipped and a corresponding notification is shown.
|
||||
//
|
||||
// Full containment of the perimeter within modifier is not handled.
|
||||
//
|
||||
// FUTURE DIRECTION:
|
||||
// A lightweight algorithm is needed to detect and handle through-body intersections
|
||||
// for Weak modifiers. The algorithm must not slow down the 99.9% common case.
|
||||
// Possible approach: if intersection passes the diff check (no through-body),
|
||||
// use the current fast algorithm. If diff check fails, fall back to a heavier
|
||||
// method: compute midpoints of intersection polygon edges, then check which
|
||||
// midpoints lie strictly inside the modifier (not on boundary) using
|
||||
// point_in_polygon. Those edges originate from the perimeter; the rest
|
||||
// originate from the modifier boundary. Collect perimeter edges into a polyline.
|
||||
// Additionally, multiply-connected cross-sections could be supported instead of
|
||||
// being skipped entirely (e.g. by decomposing them into simple polygons).
|
||||
// For Enforced and Neutral weak modifiers, full containment of the perimeter
|
||||
// within modifier could be handled (currently ignored).
|
||||
|
||||
namespace Slic3r {
|
||||
namespace PreciseSeam {
|
||||
|
||||
// Import EnforcedBlockedSeamPoint from SeamPlacerImpl namespace for convenience
|
||||
using SeamPlacerImpl::EnforcedBlockedSeamPoint;
|
||||
|
||||
// Pre-sliced modifier cache: ModelVolume pointer → per-layer Polygons.
|
||||
// Built once in SeamPlacer::init(), then passed read-only into per-perimeter functions.
|
||||
using ModifierSlicesCache = std::unordered_map<const ModelVolume*, std::vector<Polygons>>;
|
||||
|
||||
// Warning flags set during Precise Seam processing (thread-safe)
|
||||
struct PreciseSeamWarnings {
|
||||
std::atomic<bool> multiple_intersections{false}; // modifier intersects perimeter in multiple separate places (strong only)
|
||||
std::atomic<bool> through_body{false}; // modifier passes through the model body entirely
|
||||
std::atomic<bool> multiply_connected{false}; // modifier has holes (multiply-connected cross-section)
|
||||
std::atomic<bool> full_containment{false}; // modifier fully contains perimeter, no intersection edges
|
||||
};
|
||||
|
||||
// Result of finding common segment between perimeter and intersection
|
||||
struct SegmentData {
|
||||
Polyline segment; // Points from intersection_polygon forming the segment
|
||||
std::vector<size_t> perimeter_edge_indices; // edge_index for each point in segment
|
||||
};
|
||||
|
||||
// Result of weak modifier segment processing
|
||||
struct WeakModifierSegment {
|
||||
EnforcedBlockedSeamPoint type; // Enforced/Blocked/Neutral
|
||||
Point left_point; // Coordinates of left (first) point of segment
|
||||
size_t left_idx; // Perimeter vertex index for left_point
|
||||
Point right_point; // Coordinates of right (last) point of segment
|
||||
size_t right_idx; // Perimeter vertex index for right_point
|
||||
};
|
||||
|
||||
// Initialize Precise Seam data by populating provided vectors and flag
|
||||
// Collects precise seam modifiers and fills output parameters
|
||||
// Call once during SeamPlacer::init() before gather_seam_candidates()
|
||||
// Parameters:
|
||||
// strong_volumes_out - output vector for strong modifiers (CENTER/LEFT/RIGHT)
|
||||
// weak_volumes_out - output vector for weak modifiers (ENFORCED/BLOCKED/NEUTRAL)
|
||||
// has_strong_out - output flag indicating presence of strong modifiers
|
||||
// model_object - model object containing volumes
|
||||
void init_precise_seam_data(
|
||||
std::vector<const ModelVolume*>& strong_volumes_out,
|
||||
std::vector<const ModelVolume*>& weak_volumes_out,
|
||||
bool& has_strong_out,
|
||||
const ModelObject* model_object);
|
||||
|
||||
// Insert strong seam point into perimeter polygon
|
||||
// Processes strong modifiers (CENTER/LEFT/RIGHT) and inserts seam point into polygon
|
||||
// Parameters:
|
||||
// strong_volumes - list of strong precise seam modifiers
|
||||
// polygon - perimeter polygon (will be modified if point inserted)
|
||||
// layer - current layer
|
||||
// slices_cache - pre-sliced modifier polygons (built once in SeamPlacer::init)
|
||||
// Returns:
|
||||
// Coordinates of inserted point (internal units) or std::nullopt if nothing inserted
|
||||
std::optional<Point> insert_strong_seam_point(
|
||||
const std::vector<const ModelVolume*> &strong_volumes,
|
||||
Polygon &polygon,
|
||||
const Layer *layer,
|
||||
const ModifierSlicesCache &slices_cache,
|
||||
PreciseSeamWarnings* warnings = nullptr);
|
||||
|
||||
// Collect all weak modifier segments for a perimeter polygon
|
||||
// Processes weak modifiers (ENFORCED/BLOCKED/NEUTRAL) and collects segment boundaries
|
||||
// Also inserts boundary points into the perimeter polygon (sorted by descending arc length)
|
||||
// Refines enforced edges by subdividing them into segments ≤ enforcer_oversampling_distance
|
||||
// Parameters:
|
||||
// weak_volumes - list of weak precise seam modifiers
|
||||
// polygon - perimeter polygon (will be modified with inserted points and refined edges)
|
||||
// layer - current layer
|
||||
// slices_cache - pre-sliced modifier polygons (built once in SeamPlacer::init)
|
||||
// Returns:
|
||||
// Ordered vector of segments with updated coordinates (same order as weak_volumes list)
|
||||
std::vector<WeakModifierSegment> collect_weak_modifier_segments(
|
||||
const std::vector<const ModelVolume*> &weak_volumes,
|
||||
Polygon &polygon,
|
||||
const Layer *layer,
|
||||
const ModifierSlicesCache &slices_cache,
|
||||
PreciseSeamWarnings* warnings = nullptr);
|
||||
|
||||
// Apply weak modifier types to perimeter points based on segment boundaries
|
||||
// Finds boundary points in refined polygon and sets types for points within segments
|
||||
// Parameters:
|
||||
// weak_segments - segments with boundary coordinates and types
|
||||
// result - layer seams data to modify
|
||||
// perimeter - perimeter info (start/end indices)
|
||||
// some_point_enforced - flag to update if Enforced points are set
|
||||
void apply_weak_modifiers_to_perimeter(
|
||||
const std::vector<WeakModifierSegment> &weak_segments,
|
||||
PrintObjectSeamData::LayerSeams &result,
|
||||
const SeamPlacerImpl::Perimeter &perimeter,
|
||||
bool &some_point_enforced);
|
||||
|
||||
// Restore precise seam positions that may have been modified by alignment
|
||||
// Iterates through all perimeters and restores precise_seam_point positions
|
||||
void restore_precise_seam_positions(std::vector<PrintObjectSeamData::LayerSeams> &layers);
|
||||
|
||||
} // namespace PreciseSeam
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_PreciseSeam_hpp_
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <random>
|
||||
#include <algorithm>
|
||||
#include <queue>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "libslic3r/AABBTreeLines.hpp"
|
||||
#include "libslic3r/KDTreeIndirect.hpp"
|
||||
@@ -19,10 +20,12 @@
|
||||
#include "libslic3r/Layer.hpp"
|
||||
|
||||
#include "libslic3r/Geometry/Curves.hpp"
|
||||
#include "libslic3r/I18N.hpp"
|
||||
#include "libslic3r/ShortEdgeCollapse.hpp"
|
||||
#include "libslic3r/TriangleSetSampling.hpp"
|
||||
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "PreciseSeam.hpp"
|
||||
|
||||
//#define DEBUG_FILES
|
||||
|
||||
@@ -303,6 +306,16 @@ struct GlobalModelInfo {
|
||||
AABBTreeIndirect::Tree<3, float> enforcers_tree;
|
||||
AABBTreeIndirect::Tree<3, float> blockers_tree;
|
||||
|
||||
// Precise Seam modifiers: strong modifiers (CENTER/LEFT/RIGHT) determine exact seam placement
|
||||
std::vector<const ModelVolume*> precise_seam_strong_volumes;
|
||||
// Precise Seam modifiers: weak modifiers (ENFORCED/BLOCKED/NEUTRAL) provide hints for seam placement
|
||||
std::vector<const ModelVolume*> precise_seam_weak_volumes;
|
||||
|
||||
// Pre-sliced modifier polygons, keyed by ModelVolume pointer.
|
||||
// Populated once in SeamPlacer::init() to avoid re-slicing on every perimeter.
|
||||
// Each value is a per-layer vector of Polygons for that modifier volume.
|
||||
std::unordered_map<const ModelVolume*, std::vector<Polygons>> precise_seam_slices;
|
||||
|
||||
bool is_enforced(const Vec3f &position, float radius) const {
|
||||
if (enforcers.empty()) {
|
||||
return false;
|
||||
@@ -403,7 +416,8 @@ struct GlobalModelInfo {
|
||||
;
|
||||
|
||||
//Extract perimeter polygons of the given layer
|
||||
Polygons extract_perimeter_polygons(const Layer *layer, std::vector<const LayerRegion*> &corresponding_regions_out) {
|
||||
Polygons extract_perimeter_polygons(const Layer *layer, std::vector<const LayerRegion*> &corresponding_regions_out,
|
||||
bool has_precise_seam_modifiers) {
|
||||
Polygons polygons;
|
||||
for (const LayerRegion *layer_region : layer->regions()) {
|
||||
for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) {
|
||||
@@ -440,6 +454,18 @@ Polygons extract_perimeter_polygons(const Layer *layer, std::vector<const LayerR
|
||||
}
|
||||
}
|
||||
|
||||
if (has_precise_seam_modifiers) {
|
||||
// Extrusion loops repeat their start point; Polygon closes the contour implicitly.
|
||||
// Normalize here for Precise Seam without changing ordinary seam candidates.
|
||||
for (Polygon &polygon : polygons) {
|
||||
// Adjacent extrusion paths share endpoints; zero-length edges would prevent refinement at their junctions.
|
||||
// Remove only consecutive duplicates, preserving distinct visits to a self-touching contour point.
|
||||
polygon.points.erase(std::unique(polygon.points.begin(), polygon.points.end()), polygon.points.end());
|
||||
while (polygon.size() > 1 && polygon.points.front() == polygon.points.back())
|
||||
polygon.points.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
if (polygons.empty()) { // If there are no perimeter polygons for whatever reason (disabled perimeters .. ) insert dummy point
|
||||
// it is easier than checking everywhere if the layer is not emtpy, no seam will be placed to this layer anyway
|
||||
polygons.emplace_back(Points{ { 0, 0 } });
|
||||
@@ -449,17 +475,42 @@ Polygons extract_perimeter_polygons(const Layer *layer, std::vector<const LayerR
|
||||
return polygons;
|
||||
}
|
||||
|
||||
// Insert SeamCandidates created from perimeter polygons in to the result vector.
|
||||
// Compute its type (Enfrocer,Blocker), angle, and position
|
||||
//each SeamCandidate also contains pointer to shared Perimeter structure representing the polygon
|
||||
// if Custom Seam modifiers are present, oversamples the polygon if necessary to better fit user intentions
|
||||
// Build SeamCandidates for each vertex of the perimeter polygon and attach them to a shared Perimeter.
|
||||
// For each vertex: computes position, angle, and type (Enforcer / Blocker / Neutral).
|
||||
// When Precise Seam modifiers are present: inserts strong seam point,
|
||||
// oversamples enforcer edges, applies weak modifiers, marks one enforced point as central for alignment.
|
||||
void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const LayerRegion *region,
|
||||
const GlobalModelInfo &global_model_info, PrintObjectSeamData::LayerSeams &result) {
|
||||
const GlobalModelInfo &global_model_info, PrintObjectSeamData::LayerSeams &result,
|
||||
PreciseSeam::PreciseSeamWarnings* warnings = nullptr) {
|
||||
if (orig_polygon.size() == 0) {
|
||||
return;
|
||||
}
|
||||
Polygon polygon = orig_polygon;
|
||||
bool was_clockwise = polygon.make_counter_clockwise();
|
||||
|
||||
// Process Precise Seam modifiers to find seam placement
|
||||
const Layer* layer = region ? region->layer() : nullptr;
|
||||
|
||||
// Use pre-computed Precise Seam volumes from global_model_info (computed once in init)
|
||||
const auto& strong_volumes = global_model_info.precise_seam_strong_volumes;
|
||||
const auto& weak_volumes = global_model_info.precise_seam_weak_volumes;
|
||||
|
||||
// Use pre-sliced cache from global_model_info instead of re-slicing on every call
|
||||
auto seam_point = PreciseSeam::insert_strong_seam_point(strong_volumes, polygon, layer, global_model_info.precise_seam_slices, warnings);
|
||||
|
||||
// Store the inserted point position for marking as central_enforcer later
|
||||
std::optional<Vec3f> inserted_seam_position;
|
||||
if (seam_point.has_value()) {
|
||||
Vec2f unscaled_p = unscale(seam_point.value()).cast<float>();
|
||||
inserted_seam_position = Vec3f(unscaled_p.x(), unscaled_p.y(), z_coord);
|
||||
}
|
||||
|
||||
// Process weak modifiers (ENFORCED/BLOCKED/NEUTRAL) only if no strong modifier was inserted
|
||||
std::vector<PreciseSeam::WeakModifierSegment> weak_segments;
|
||||
if (!inserted_seam_position.has_value()) {
|
||||
weak_segments = PreciseSeam::collect_weak_modifier_segments(weak_volumes, polygon, layer, global_model_info.precise_seam_slices, warnings);
|
||||
}
|
||||
|
||||
float angle_arm_len = region != nullptr ? region->flow(FlowRole::frExternalPerimeter).nozzle_diameter() : 0.5f;
|
||||
|
||||
std::vector<float> lengths { };
|
||||
@@ -528,10 +579,15 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
|
||||
|
||||
perimeter.end_index = result.points.size();
|
||||
|
||||
// Apply weak modifiers if no strong modifier was inserted
|
||||
if (!inserted_seam_position.has_value() && !weak_segments.empty()) {
|
||||
PreciseSeam::apply_weak_modifiers_to_perimeter(
|
||||
weak_segments, result, perimeter, some_point_enforced);
|
||||
}
|
||||
|
||||
if (some_point_enforced) {
|
||||
// We will patches of enforced points (patch: continuous section of enforced points), choose
|
||||
// the longest patch, and select the middle point or sharp point (depending on the angle)
|
||||
// this point will have high priority on this perimeter
|
||||
// Choose the continuous enforced patch with the most candidates, then select its middle
|
||||
// candidate or a sharp corner. Patch length here is a point count, not geometric distance.
|
||||
size_t perimeter_size = perimeter.end_index - perimeter.start_index;
|
||||
const auto next_index = [&](size_t idx) {
|
||||
return perimeter.start_index + Slic3r::next_idx_modulo(idx - perimeter.start_index, perimeter_size);
|
||||
@@ -562,7 +618,9 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
|
||||
std::pair<size_t, size_t> longest_patch { 0, 0 };
|
||||
auto patch_len = [perimeter_size](const std::pair<size_t, size_t> &start_end) {
|
||||
if (start_end.second < start_end.first) {
|
||||
return start_end.first + (perimeter_size - start_end.second);
|
||||
// Count [start, end) across the closing edge, independently of the contour's start.
|
||||
// Subtract indices first: they are offsets in the layer, not local perimeter indices.
|
||||
return perimeter_size - (start_end.first - start_end.second);
|
||||
} else {
|
||||
return start_end.second - start_end.first;
|
||||
}
|
||||
@@ -595,30 +653,24 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Get index of previous and next perimeter point of the layer. Because SeamCandidates of all polygons of the given layer
|
||||
// are sequentially stored in the vector, each perimeter contains info about start and end index. These vales are used to
|
||||
// deduce index of previous and next neigbour in the corresponding perimeter.
|
||||
std::pair<size_t, size_t> find_previous_and_next_perimeter_point(const std::vector<SeamCandidate> &perimeter_points,
|
||||
size_t point_index) {
|
||||
const SeamCandidate ¤t = perimeter_points[point_index];
|
||||
int prev = point_index - 1; //for majority of points, it is true that neighbours lie behind and in front of them in the vector
|
||||
int next = point_index + 1;
|
||||
|
||||
if (point_index == current.perimeter.start_index) {
|
||||
// if point_index is equal to start, it means that the previous neighbour is at the end
|
||||
prev = current.perimeter.end_index;
|
||||
// Apply precise seam point if it was inserted
|
||||
if (inserted_seam_position.has_value()) {
|
||||
// Set single point as Enforced, block all others
|
||||
for (size_t i = perimeter.start_index; i < perimeter.end_index; ++i) {
|
||||
if (result.points[i].position == inserted_seam_position.value()) {
|
||||
// Mark as the single enforced point with highest priority
|
||||
result.points[i].type = EnforcedBlockedSeamPoint::Enforced;
|
||||
result.points[i].central_enforcer = true;
|
||||
perimeter.precise_seam_point = inserted_seam_position;
|
||||
perimeter.precise_seam_index = i;
|
||||
} else {
|
||||
// Block all other points
|
||||
result.points[i].type = EnforcedBlockedSeamPoint::Blocked;
|
||||
result.points[i].central_enforcer = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (point_index == current.perimeter.end_index - 1) {
|
||||
// if point_index is equal to end, than next neighbour is at the start
|
||||
next = current.perimeter.start_index;
|
||||
}
|
||||
|
||||
assert(prev >= 0);
|
||||
assert(next >= 0);
|
||||
return {size_t(prev),size_t(next)};
|
||||
}
|
||||
|
||||
// Computes all global model info - transforms object, performs raycasting
|
||||
@@ -636,7 +688,9 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po,
|
||||
|| model_volume->type() == ModelVolumeType::NEGATIVE_VOLUME) {
|
||||
auto model_transformation = model_volume->get_matrix();
|
||||
indexed_triangle_set model_its = model_volume->mesh().its;
|
||||
// ORCA: Mirrored transforms flip winding, keep normals outward
|
||||
// ORCA fix (not related to Precise Seam, discovered during its development):
|
||||
// Mirror transforms have negative determinant which flips triangle winding.
|
||||
// fix_left_handed=true swaps indices to keep normals pointing outward.
|
||||
its_transform(model_its, model_transformation, true);
|
||||
if (model_volume->type() == ModelVolumeType::MODEL_PART) {
|
||||
its_merge(triangle_set, model_its);
|
||||
@@ -657,7 +711,10 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po,
|
||||
|
||||
size_t negative_volumes_start_index = triangle_set.indices.size();
|
||||
its_merge(triangle_set, negative_volumes_set);
|
||||
// ORCA: Mirroring flips normals, keep them outward for visibility sampling
|
||||
// ORCA fix (not related to Precise Seam, discovered during its development):
|
||||
// Object-level transform may include mirroring (negative determinant),
|
||||
// which inverts triangle winding. fix_left_handed=true corrects this
|
||||
// so visibility ray sampling sees outward-facing normals.
|
||||
its_transform(triangle_set, obj_transform, true);
|
||||
BOOST_LOG_TRIVIAL(debug)
|
||||
<< "SeamPlacer: decimate: end";
|
||||
@@ -719,12 +776,12 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) {
|
||||
auto model_transformation = obj_transform * mv->get_matrix();
|
||||
|
||||
indexed_triangle_set enforcers = mv->seam_facets.get_facets(*mv, EnforcerBlockerType::ENFORCER);
|
||||
// ORCA: Keep normals outward when mirroring seam enforcers
|
||||
// ORCA fix (not related to Precise Seam): fix winding for mirrored transforms
|
||||
its_transform(enforcers, model_transformation, true);
|
||||
its_merge(result.enforcers, enforcers);
|
||||
|
||||
indexed_triangle_set blockers = mv->seam_facets.get_facets(*mv, EnforcerBlockerType::BLOCKER);
|
||||
// ORCA: Keep normals outward when mirroring seam blockers
|
||||
// ORCA fix (not related to Precise Seam): fix winding for mirrored transforms
|
||||
its_transform(blockers, model_transformation, true);
|
||||
its_merge(result.blockers, blockers);
|
||||
}
|
||||
@@ -1011,13 +1068,14 @@ void pick_random_seam_point(const std::vector<SeamCandidate> &perimeter_points,
|
||||
// Parallel process and extract each perimeter polygon of the given print object.
|
||||
// Gather SeamCandidates of each layer into vector and build KDtree over them
|
||||
// Store results in the SeamPlacer variables m_seam_per_object
|
||||
void SeamPlacer::gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info) {
|
||||
void SeamPlacer::gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info,
|
||||
PreciseSeam::PreciseSeamWarnings* warnings) {
|
||||
using namespace SeamPlacerImpl;
|
||||
PrintObjectSeamData &seam_data = m_seam_per_object.emplace(po, PrintObjectSeamData { }).first->second;
|
||||
seam_data.layers.resize(po->layer_count());
|
||||
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, po->layers().size()),
|
||||
[po, &global_model_info, &seam_data]
|
||||
[po, &global_model_info, &seam_data, warnings]
|
||||
(tbb::blocked_range<size_t> r) {
|
||||
for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) {
|
||||
PrintObjectSeamData::LayerSeams &layer_seams = seam_data.layers[layer_idx];
|
||||
@@ -1025,10 +1083,13 @@ void SeamPlacer::gather_seam_candidates(const PrintObject *po, const SeamPlacerI
|
||||
auto unscaled_z = layer->slice_z;
|
||||
std::vector<const LayerRegion*> regions;
|
||||
//NOTE corresponding region ptr may be null, if the layer has zero perimeters
|
||||
Polygons polygons = extract_perimeter_polygons(layer, regions);
|
||||
const bool has_precise_seam_modifiers = !global_model_info.precise_seam_strong_volumes.empty() ||
|
||||
!global_model_info.precise_seam_weak_volumes.empty();
|
||||
Polygons polygons = extract_perimeter_polygons(layer, regions, has_precise_seam_modifiers);
|
||||
for (size_t poly_index = 0; poly_index < polygons.size(); ++poly_index) {
|
||||
process_perimeter_polygon(polygons[poly_index], unscaled_z,
|
||||
regions[poly_index], global_model_info, layer_seams);
|
||||
regions[poly_index], global_model_info, layer_seams,
|
||||
warnings);
|
||||
}
|
||||
auto functor = SeamCandidateCoordinateFunctor { layer_seams.points };
|
||||
seam_data.layers[layer_idx].points_tree =
|
||||
@@ -1424,10 +1485,13 @@ void SeamPlacer::align_seam_points(const PrintObject *po, const SeamPlacerImpl::
|
||||
|
||||
}
|
||||
|
||||
void SeamPlacer::init(const Print &print, std::function<void(void)> throw_if_canceled_func) {
|
||||
void SeamPlacer::init(Print &print, std::function<void(void)> throw_if_canceled_func) {
|
||||
using namespace SeamPlacerImpl;
|
||||
m_seam_per_object.clear();
|
||||
|
||||
// Warning flags for Precise Seam processing — shared across all objects
|
||||
PreciseSeam::PreciseSeamWarnings precise_seam_warnings;
|
||||
|
||||
for (const PrintObject *po : print.objects()) {
|
||||
throw_if_canceled_func();
|
||||
SeamPosition configured_seam_preference = po->config().seam_position.value;
|
||||
@@ -1436,14 +1500,29 @@ void SeamPlacer::init(const Print &print, std::function<void(void)> throw_if_can
|
||||
{
|
||||
GlobalModelInfo global_model_info { };
|
||||
gather_enforcers_blockers(global_model_info, po);
|
||||
PreciseSeam::init_precise_seam_data(
|
||||
global_model_info.precise_seam_strong_volumes,
|
||||
global_model_info.precise_seam_weak_volumes,
|
||||
m_seam_per_object[po].has_precise_seam_strong_volumes,
|
||||
po->model_object());
|
||||
|
||||
// Pre-slice all precise seam modifier volumes once per object.
|
||||
// Without this cache, slice_single_volume() would be called for every
|
||||
// modifier × every perimeter × every layer — thousands of redundant slicing operations.
|
||||
for (const ModelVolume* vol : global_model_info.precise_seam_strong_volumes)
|
||||
global_model_info.precise_seam_slices[vol] = po->slice_single_volume(vol);
|
||||
for (const ModelVolume* vol : global_model_info.precise_seam_weak_volumes)
|
||||
global_model_info.precise_seam_slices[vol] = po->slice_single_volume(vol);
|
||||
|
||||
throw_if_canceled_func();
|
||||
if (configured_seam_preference == spAligned || configured_seam_preference == spNearest || configured_seam_preference == spAlignedBack) {
|
||||
compute_global_occlusion(global_model_info, po, throw_if_canceled_func, configured_seam_preference);
|
||||
}
|
||||
throw_if_canceled_func();
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug)
|
||||
<< "SeamPlacer: gather_seam_candidates: start";
|
||||
gather_seam_candidates(po, global_model_info);
|
||||
gather_seam_candidates(po, global_model_info, &precise_seam_warnings);
|
||||
BOOST_LOG_TRIVIAL(debug)
|
||||
<< "SeamPlacer: gather_seam_candidates: end";
|
||||
throw_if_canceled_func();
|
||||
@@ -1491,10 +1570,49 @@ void SeamPlacer::init(const Print &print, std::function<void(void)> throw_if_can
|
||||
<< "SeamPlacer: align_seam_points : end";
|
||||
}
|
||||
|
||||
// Restore precise seam positions that were potentially modified
|
||||
if (m_seam_per_object[po].has_precise_seam_strong_volumes) {
|
||||
PreciseSeam::restore_precise_seam_positions(m_seam_per_object[po].layers);
|
||||
}
|
||||
|
||||
#ifdef DEBUG_FILES
|
||||
debug_export_points(m_seam_per_object[po].layers, po->bounding_box(), comparator);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Show Precise Seam warnings (once for all objects).
|
||||
// Only ONE active_step_add_warning() call — multiple calls generate multiple UI events,
|
||||
// each re-pushing ALL current warnings via Plater handler, causing NotificationManager::append()
|
||||
// to duplicate text within each popup.
|
||||
{
|
||||
const bool mi = precise_seam_warnings.multiple_intersections.load(std::memory_order_relaxed);
|
||||
const bool tb = precise_seam_warnings.through_body.load(std::memory_order_relaxed);
|
||||
const bool mc = precise_seam_warnings.multiply_connected.load(std::memory_order_relaxed);
|
||||
const bool fc = precise_seam_warnings.full_containment.load(std::memory_order_relaxed);
|
||||
std::vector<std::string> parts;
|
||||
if (mi)
|
||||
parts.push_back(_u8L("multiple intersections with a perimeter detected"));
|
||||
if (tb)
|
||||
parts.push_back(_u8L("modifier fully crosses the printable perimeter"));
|
||||
if (mc)
|
||||
parts.push_back(_u8L("modifier shape is not solid (has holes inside) and was ignored"));
|
||||
if (fc)
|
||||
parts.push_back(_u8L("perimeter is fully contained inside modifier and was ignored"));
|
||||
if (!parts.empty()) {
|
||||
// One line: the export warnings dialog shows only the first line of each warning.
|
||||
std::string warning_text = _u8L("Precise Seam") + ": ";
|
||||
for (size_t i = 0; i < parts.size(); ++i) {
|
||||
if (i > 0) warning_text += "; ";
|
||||
warning_text += parts[i];
|
||||
}
|
||||
warning_text += ". ";
|
||||
warning_text += _u8L("Seam placement may differ from expected.");
|
||||
print.active_step_add_warning(
|
||||
PrintStateBase::WarningLevel::NON_CRITICAL,
|
||||
warning_text,
|
||||
PrintStateBase::SlicingPreciseSeamWarning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SeamPlacer::place_seam(const Layer *layer, ExtrusionLoop &loop,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
namespace Slic3r {
|
||||
|
||||
class PrintObject;
|
||||
namespace PreciseSeam { struct PreciseSeamWarnings; }
|
||||
class ExtrusionLoop;
|
||||
class Print;
|
||||
class Layer;
|
||||
@@ -41,7 +42,7 @@ enum class EnforcedBlockedSeamPoint {
|
||||
// struct representing single perimeter loop
|
||||
struct Perimeter {
|
||||
size_t start_index{};
|
||||
size_t end_index{}; //inclusive!
|
||||
size_t end_index{}; //exclusive (one-past-the-end)
|
||||
size_t seam_index{};
|
||||
float flow_width{};
|
||||
|
||||
@@ -50,6 +51,10 @@ struct Perimeter {
|
||||
// Random position also uses this flexibility to set final seam point position
|
||||
bool finalized = false;
|
||||
Vec3f final_seam_position = Vec3f::Zero();
|
||||
|
||||
// Stores precise seam coordinates found by Precise Seam modifiers
|
||||
std::optional<Vec3f> precise_seam_point;
|
||||
size_t precise_seam_index{};
|
||||
};
|
||||
|
||||
//Struct over which all processing of perimeters is done. For each perimeter point, its respective candidate is created,
|
||||
@@ -102,6 +107,9 @@ struct PrintObjectSeamData
|
||||
// Map of PrintObjects (PO) -> vector of layers of PO -> unique_ptr to KD
|
||||
// tree of all points of the given layer
|
||||
|
||||
// Indicates presence of strong Precise Seam modifiers (CENTER/LEFT/RIGHT) for this object
|
||||
bool has_precise_seam_strong_volumes = false;
|
||||
|
||||
void clear()
|
||||
{
|
||||
layers.clear();
|
||||
@@ -141,11 +149,12 @@ public:
|
||||
//The following data structures hold all perimeter points for all PrintObject.
|
||||
std::unordered_map<const PrintObject*, PrintObjectSeamData> m_seam_per_object;
|
||||
|
||||
void init(const Print &print, std::function<void(void)> throw_if_canceled_func);
|
||||
void init(Print &print, std::function<void(void)> throw_if_canceled_func);
|
||||
|
||||
void place_seam(const Layer *layer, ExtrusionLoop &loop, const Point &last_pos, float& overhang) const;
|
||||
private:
|
||||
void gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info);
|
||||
void gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info,
|
||||
PreciseSeam::PreciseSeamWarnings* warnings = nullptr);
|
||||
void calculate_candidates_visibility(const PrintObject *po,
|
||||
const SeamPlacerImpl::GlobalModelInfo &global_model_info);
|
||||
void calculate_overhangs_and_layer_embedding(const PrintObject *po);
|
||||
|
||||
@@ -13,8 +13,19 @@
|
||||
namespace Slic3r {
|
||||
|
||||
|
||||
// How many setters this thread holds, so the ones nested in another can skip
|
||||
// setlocale, which takes a lock the whole process shares on Windows.
|
||||
static thread_local int s_numeric_locale_depth = 0;
|
||||
|
||||
CNumericLocalesSetter::CNumericLocalesSetter()
|
||||
{
|
||||
// Nested in another setter on this thread, whose "C" the separator check
|
||||
// confirms is still set.
|
||||
if (s_numeric_locale_depth > 0 && is_decimal_separator_point()) {
|
||||
m_nested = true;
|
||||
++ s_numeric_locale_depth;
|
||||
return;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
||||
m_orig_numeric_locale = std::setlocale(LC_NUMERIC, nullptr);
|
||||
@@ -29,12 +40,17 @@ CNumericLocalesSetter::CNumericLocalesSetter()
|
||||
m_new_locale = newlocale(LC_NUMERIC_MASK, "C", m_new_locale);
|
||||
uselocale(m_new_locale);
|
||||
#endif
|
||||
// Counted last, since the destructor does not run for a constructor that throws.
|
||||
++ s_numeric_locale_depth;
|
||||
}
|
||||
|
||||
|
||||
|
||||
CNumericLocalesSetter::~CNumericLocalesSetter()
|
||||
{
|
||||
-- s_numeric_locale_depth;
|
||||
if (m_nested)
|
||||
return;
|
||||
#ifdef _WIN32
|
||||
std::setlocale(LC_NUMERIC, m_orig_numeric_locale.data());
|
||||
#else
|
||||
|
||||
@@ -19,8 +19,13 @@ class CNumericLocalesSetter {
|
||||
public:
|
||||
CNumericLocalesSetter();
|
||||
~CNumericLocalesSetter();
|
||||
// A copy would restore the locale twice, and count down once more than up.
|
||||
CNumericLocalesSetter(const CNumericLocalesSetter&) = delete;
|
||||
CNumericLocalesSetter& operator=(const CNumericLocalesSetter&) = delete;
|
||||
|
||||
private:
|
||||
// Inside another setter on this thread, which does the setting and restoring.
|
||||
bool m_nested { false };
|
||||
#ifdef _WIN32
|
||||
std::string m_orig_numeric_locale;
|
||||
#else
|
||||
|
||||
+57
-4
@@ -23,6 +23,7 @@
|
||||
|
||||
#include "libslic3r/Geometry/ConvexHull.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <float.h>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
@@ -1229,6 +1230,7 @@ ModelObject& ModelObject::assign_copy(const ModelObject &rhs)
|
||||
this->volumes.emplace_back(new ModelVolume(*model_volume));
|
||||
this->volumes.back()->set_model_object(this);
|
||||
}
|
||||
|
||||
this->clear_instances();
|
||||
this->instances.reserve(rhs.instances.size());
|
||||
for (const ModelInstance *model_instance : rhs.instances) {
|
||||
@@ -1267,6 +1269,7 @@ ModelObject& ModelObject::assign_copy(ModelObject &&rhs)
|
||||
rhs.volumes.clear();
|
||||
for (ModelVolume *model_volume : this->volumes)
|
||||
model_volume->set_model_object(this);
|
||||
|
||||
this->clear_instances();
|
||||
this->instances = std::move(rhs.instances);
|
||||
rhs.instances.clear();
|
||||
@@ -1390,7 +1393,9 @@ ModelVolume* ModelObject::add_volume_with_shared_mesh(const ModelVolume &other,
|
||||
void ModelObject::delete_volume(size_t idx)
|
||||
{
|
||||
ModelVolumePtrs::iterator i = this->volumes.begin() + idx;
|
||||
delete *i;
|
||||
ModelVolume* volume_to_delete = *i;
|
||||
|
||||
delete volume_to_delete;
|
||||
this->volumes.erase(i);
|
||||
|
||||
if (this->volumes.size() == 1)
|
||||
@@ -1449,6 +1454,20 @@ void ModelObject::sort_volumes(bool full_sort)
|
||||
// sort volumes inside the object to order "Model Part, Negative Volume, Modifier, Support Blocker and Support Enforcer. "
|
||||
if (full_sort)
|
||||
std::stable_sort(volumes.begin(), volumes.end(), [](ModelVolume* vl, ModelVolume* vr) {
|
||||
// Special handling for Precise Seam modifiers: group-based sorting with user order preservation
|
||||
if (vl->is_precise_seam() && vr->is_precise_seam()) {
|
||||
// Strong (center/left/right) always before weak (enforced/blocked/neutral)
|
||||
bool vl_strong = vl->is_precise_seam_strong();
|
||||
bool vr_strong = vr->is_precise_seam_strong();
|
||||
if (vl_strong != vr_strong)
|
||||
return vl_strong; // strong < weak → strong group appears first
|
||||
|
||||
// Within same group (both strong or both weak): preserve current order
|
||||
// stable_sort will maintain relative positions when comparator returns false
|
||||
return false;
|
||||
}
|
||||
|
||||
// For non-Precise-Seam or mixed types: use standard enum-based ordering
|
||||
return vl->type() < vr->type();
|
||||
});
|
||||
// sort have to controll "place" of the support blockers/enforcers. But one of the model parts have to be on the first place.
|
||||
@@ -1456,10 +1475,19 @@ void ModelObject::sort_volumes(bool full_sort)
|
||||
std::stable_sort(volumes.begin(), volumes.end(), [](ModelVolume* vl, ModelVolume* vr) {
|
||||
ModelVolumeType vl_type = vl->type() > ModelVolumeType::PARAMETER_MODIFIER ? vl->type() : ModelVolumeType::PARAMETER_MODIFIER;
|
||||
ModelVolumeType vr_type = vr->type() > ModelVolumeType::PARAMETER_MODIFIER ? vr->type() : ModelVolumeType::PARAMETER_MODIFIER;
|
||||
|
||||
// Apply same Precise Seam grouping logic for partial sort
|
||||
if (vl->is_precise_seam() && vr->is_precise_seam()) {
|
||||
bool vl_strong = vl->is_precise_seam_strong();
|
||||
bool vr_strong = vr->is_precise_seam_strong();
|
||||
if (vl_strong != vr_strong)
|
||||
return vl_strong;
|
||||
return false; // preserve order within same group
|
||||
}
|
||||
|
||||
return vl_type < vr_type;
|
||||
});
|
||||
}
|
||||
|
||||
ModelInstance* ModelObject::add_instance()
|
||||
{
|
||||
ModelInstance* i = new ModelInstance(this);
|
||||
@@ -2615,7 +2643,8 @@ std::vector<int> ModelVolume::get_extruders() const
|
||||
if (m_type == ModelVolumeType::INVALID
|
||||
|| m_type == ModelVolumeType::NEGATIVE_VOLUME
|
||||
|| m_type == ModelVolumeType::SUPPORT_BLOCKER
|
||||
|| m_type == ModelVolumeType::SUPPORT_ENFORCER)
|
||||
|| m_type == ModelVolumeType::SUPPORT_ENFORCER
|
||||
|| this->is_precise_seam()) // Precise Seam is non-printing helper geometry
|
||||
return std::vector<int>();
|
||||
|
||||
if (mmu_segmentation_facets.timestamp() != mmuseg_ts) {
|
||||
@@ -2819,6 +2848,19 @@ ModelVolumeType ModelVolume::type_from_string(const std::string &s)
|
||||
return ModelVolumeType::SUPPORT_ENFORCER;
|
||||
if (s == "support_blocker")
|
||||
return ModelVolumeType::SUPPORT_BLOCKER;
|
||||
// Precise Seam types
|
||||
if (s == "precise_seam_center")
|
||||
return ModelVolumeType::PRECISE_SEAM_CENTER;
|
||||
if (s == "precise_seam_left")
|
||||
return ModelVolumeType::PRECISE_SEAM_LEFT;
|
||||
if (s == "precise_seam_right")
|
||||
return ModelVolumeType::PRECISE_SEAM_RIGHT;
|
||||
if (s == "precise_seam_enforced")
|
||||
return ModelVolumeType::PRECISE_SEAM_ENFORCED;
|
||||
if (s == "precise_seam_blocked")
|
||||
return ModelVolumeType::PRECISE_SEAM_BLOCKED;
|
||||
if (s == "precise_seam_neutral")
|
||||
return ModelVolumeType::PRECISE_SEAM_NEUTRAL;
|
||||
//assert(s == "0");
|
||||
// Default value if invalud type string received.
|
||||
return ModelVolumeType::MODEL_PART;
|
||||
@@ -2833,6 +2875,12 @@ std::string ModelVolume::type_to_string(const ModelVolumeType t)
|
||||
case ModelVolumeType::PARAMETER_MODIFIER: return "modifier_part";
|
||||
case ModelVolumeType::SUPPORT_ENFORCER: return "support_enforcer";
|
||||
case ModelVolumeType::SUPPORT_BLOCKER: return "support_blocker";
|
||||
case ModelVolumeType::PRECISE_SEAM_CENTER: return "precise_seam_center";
|
||||
case ModelVolumeType::PRECISE_SEAM_LEFT: return "precise_seam_left";
|
||||
case ModelVolumeType::PRECISE_SEAM_RIGHT: return "precise_seam_right";
|
||||
case ModelVolumeType::PRECISE_SEAM_ENFORCED: return "precise_seam_enforced";
|
||||
case ModelVolumeType::PRECISE_SEAM_BLOCKED: return "precise_seam_blocked";
|
||||
case ModelVolumeType::PRECISE_SEAM_NEUTRAL: return "precise_seam_neutral";
|
||||
default:
|
||||
assert(false);
|
||||
return "normal_part";
|
||||
@@ -3702,7 +3750,11 @@ void FacetsAnnotation::set_triangle_from_string(int triangle_id, const std::stri
|
||||
m_data.bitstream.insert(m_data.bitstream.end(), bool(dec & (1 << i)));
|
||||
}
|
||||
|
||||
m_data.update_used_states(bitstream_start_idx);
|
||||
if (!m_data.update_used_states(bitstream_start_idx)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": dropping malformed paint data of triangle " << triangle_id;
|
||||
m_data.bitstream.resize(bitstream_start_idx);
|
||||
m_data.triangles_to_split.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
bool FacetsAnnotation::equals(const FacetsAnnotation &other) const
|
||||
@@ -3785,6 +3837,7 @@ bool model_volume_list_changed(const ModelObject &model_object_old, const ModelO
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
template< typename TypeFilterFn, typename CompareFn>
|
||||
bool model_property_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, TypeFilterFn type_filter, CompareFn compare)
|
||||
{
|
||||
|
||||
+23
-1
@@ -347,8 +347,22 @@ enum class ModelVolumeType : int {
|
||||
PARAMETER_MODIFIER,
|
||||
SUPPORT_BLOCKER,
|
||||
SUPPORT_ENFORCER,
|
||||
// Precise seam modifiers (6 subtypes for seam placement control).
|
||||
// Order is critical: strong types first, then weak. Range checks in is_precise_seam*() depend on it.
|
||||
PRECISE_SEAM_CENTER,
|
||||
PRECISE_SEAM_LEFT,
|
||||
PRECISE_SEAM_RIGHT,
|
||||
PRECISE_SEAM_ENFORCED,
|
||||
PRECISE_SEAM_BLOCKED,
|
||||
PRECISE_SEAM_NEUTRAL,
|
||||
};
|
||||
|
||||
// Free functions for checking ModelVolumeType without a ModelVolume object.
|
||||
// Keep in sync with ModelVolume::is_precise_seam*() methods below.
|
||||
inline bool is_precise_seam(ModelVolumeType t) { return t >= ModelVolumeType::PRECISE_SEAM_CENTER && t <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
|
||||
inline bool is_precise_seam_strong(ModelVolumeType t) { return t >= ModelVolumeType::PRECISE_SEAM_CENTER && t <= ModelVolumeType::PRECISE_SEAM_RIGHT; }
|
||||
inline bool is_precise_seam_weak(ModelVolumeType t) { return t >= ModelVolumeType::PRECISE_SEAM_ENFORCED && t <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
|
||||
|
||||
// A printable object, possibly having multiple print volumes (each with its own set of parameters and materials),
|
||||
// and possibly having multiple modifier volumes, each modifier volume with its set of parameters and materials.
|
||||
// Each ModelObject may be instantiated mutliple times, each instance having different placement on the print bed,
|
||||
@@ -917,6 +931,14 @@ public:
|
||||
bool is_support_enforcer() const { return m_type == ModelVolumeType::SUPPORT_ENFORCER; }
|
||||
bool is_support_blocker() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER; }
|
||||
bool is_support_modifier() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER || m_type == ModelVolumeType::SUPPORT_ENFORCER; }
|
||||
// Check if this volume is any of the precise seam modifier subtypes
|
||||
bool is_precise_seam() const { return m_type >= ModelVolumeType::PRECISE_SEAM_CENTER && m_type <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
|
||||
// Helper to check if volume is a "strong" Precise Seam type (center, left, right)
|
||||
// Strong modifiers have priority and always appear above weak modifiers in UI
|
||||
bool is_precise_seam_strong() const { return m_type >= ModelVolumeType::PRECISE_SEAM_CENTER && m_type <= ModelVolumeType::PRECISE_SEAM_RIGHT; }
|
||||
// Helper to check if volume is a "weak" Precise Seam type (enforced, blocked, neutral)
|
||||
// Weak modifiers always appear below strong modifiers in UI
|
||||
bool is_precise_seam_weak() const { return m_type >= ModelVolumeType::PRECISE_SEAM_ENFORCED && m_type <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
|
||||
bool is_text() const { return text_configuration.has_value(); }
|
||||
bool is_svg() const { return emboss_shape.has_value() && !text_configuration.has_value(); }
|
||||
bool is_the_only_one_part() const; // behave like an object
|
||||
@@ -1041,7 +1063,7 @@ protected:
|
||||
friend class SLAPrint;
|
||||
friend class Model;
|
||||
friend class ModelObject;
|
||||
friend void model_volume_list_update_supports(ModelObject& model_object_dst, const ModelObject& model_object_new);
|
||||
friend void model_volume_list_update_supports_and_seams(ModelObject& model_object_dst, const ModelObject& model_object_new);
|
||||
|
||||
// Copies IDs of both the ModelVolume and its config.
|
||||
explicit ModelVolume(const ModelVolume &rhs) = default;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
#include <tbb/task_arena.h>
|
||||
#include <tbb/task_group.h>
|
||||
|
||||
#include "Exception.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// A batch of resolved presets, each a whole config, is what resolve_then_commit adds
|
||||
// to peak memory, so it stays far below a vendor's preset count and above any core count.
|
||||
inline constexpr size_t resolve_batch_size = 64;
|
||||
|
||||
// Resolve `count` items that do not depend on each other and install them one at
|
||||
// a time.
|
||||
//
|
||||
// `resolve(i)` runs on any thread and must read only, since the items are
|
||||
// resolved side by side. `commit(i, resolved)` is called for every item in index
|
||||
// order, on the calling thread, and is where shared state is written.
|
||||
//
|
||||
// The items are worked through in batches, so what is resolved and held at once
|
||||
// does not grow with `count`. An exception from either callable propagates after
|
||||
// the batches before it have been committed. A cancellation of the caller's task
|
||||
// group, which stops a batch partway without an exception, throws RuntimeError
|
||||
// before that batch commits.
|
||||
//
|
||||
// `ChunkSetup`, when given, is constructed once for each piece of a batch TBB hands
|
||||
// out, for per-thread state a resolve would otherwise set up per item, such as the
|
||||
// C numeric locale, whose setting takes a lock the whole process shares.
|
||||
template<class ChunkSetup = std::monostate, class Resolve, class Commit>
|
||||
void resolve_then_commit(size_t count, Resolve resolve, Commit commit)
|
||||
{
|
||||
using Resolved = std::invoke_result_t<Resolve&, size_t>;
|
||||
|
||||
std::vector<Resolved> resolved(std::min(count, resolve_batch_size));
|
||||
for (size_t first = 0; first < count; first += resolve_batch_size) {
|
||||
const size_t last = std::min(first + resolve_batch_size, count);
|
||||
// Isolated, so a thread waiting on the batch runs none of the caller's other
|
||||
// tasks before finishing it.
|
||||
tbb::this_task_arena::isolate([&] {
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(first, last),
|
||||
[&](const tbb::blocked_range<size_t>& range) {
|
||||
ChunkSetup setup;
|
||||
(void) setup;
|
||||
for (size_t i = range.begin(); i < range.end(); ++ i)
|
||||
resolved[i - first] = resolve(i);
|
||||
});
|
||||
});
|
||||
if (tbb::is_current_task_group_canceling())
|
||||
throw RuntimeError("resolve_then_commit: canceled before the batch was resolved");
|
||||
for (size_t i = first; i < last; ++ i)
|
||||
commit(i, std::move(resolved[i - first]));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -851,6 +851,8 @@ namespace client
|
||||
// If true, the macro processor will evaluate just a boolean condition using the full expressive power of the macro processor.
|
||||
bool just_boolean_expression = false;
|
||||
std::string error_message;
|
||||
// Local variables declared in {if} branches that were not taken, see PlaceholderParser::check_inactive_branches.
|
||||
mutable std::set<std::string> inactive_local_variables;
|
||||
|
||||
// Table to translate symbol tag to a human readable error message.
|
||||
static std::map<std::string, std::string> tag_to_error_message;
|
||||
@@ -892,6 +894,8 @@ namespace client
|
||||
}
|
||||
// Inside a block, which is conditionally suppressed?
|
||||
bool skipping() const { return m_depth_suppressed > 0; }
|
||||
// Are variable names resolved inside the suppressed blocks too?
|
||||
bool check_inactive_names() const { return PlaceholderParser::check_inactive_branches && ! just_boolean_expression; }
|
||||
|
||||
const ConfigOption* optptr(const t_config_option_key &opt_key) const override
|
||||
{
|
||||
@@ -927,7 +931,7 @@ namespace client
|
||||
|
||||
static void legacy_variable_expansion(const MyContext *ctx, IteratorRange &opt_key, std::string &output)
|
||||
{
|
||||
if (ctx->skipping())
|
||||
if (ctx->skipping() && ! ctx->check_inactive_names())
|
||||
return;
|
||||
|
||||
std::string opt_key_str(opt_key.begin(), opt_key.end());
|
||||
@@ -949,7 +953,9 @@ namespace client
|
||||
}
|
||||
}
|
||||
if (opt == nullptr)
|
||||
ctx->throw_exception("Variable does not exist", opt_key);
|
||||
ctx->throw_exception(ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist", opt_key);
|
||||
if (ctx->skipping())
|
||||
return;
|
||||
if (opt->is_scalar()) {
|
||||
if (opt->is_nil())
|
||||
ctx->throw_exception("Trying to reference an undefined (nil) optional variable", opt_key);
|
||||
@@ -972,9 +978,10 @@ namespace client
|
||||
IteratorRange &opt_vector_index,
|
||||
std::string &output)
|
||||
{
|
||||
if (ctx->skipping())
|
||||
if (ctx->skipping() && ! ctx->check_inactive_names())
|
||||
return;
|
||||
|
||||
const char *not_found = ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist";
|
||||
std::string opt_key_str(opt_key.begin(), opt_key.end());
|
||||
const ConfigOption *opt = ctx->resolve_symbol(opt_key_str);
|
||||
if (opt == nullptr) {
|
||||
@@ -984,18 +991,20 @@ namespace client
|
||||
opt = ctx->resolve_symbol(opt_key_str);
|
||||
}
|
||||
if (opt == nullptr)
|
||||
ctx->throw_exception("Variable does not exist", opt_key);
|
||||
ctx->throw_exception(not_found, opt_key);
|
||||
}
|
||||
if (! opt->is_vector())
|
||||
ctx->throw_exception("Trying to index a scalar variable", opt_key);
|
||||
const ConfigOption *opt_index = ctx->resolve_symbol(std::string(opt_vector_index.begin(), opt_vector_index.end()));
|
||||
if (opt_index == nullptr)
|
||||
ctx->throw_exception(not_found, opt_key);
|
||||
if (opt_index->type() != coInt)
|
||||
ctx->throw_exception("Indexing variable has to be integer", opt_key);
|
||||
if (ctx->skipping())
|
||||
return;
|
||||
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt);
|
||||
if (vec->empty())
|
||||
ctx->throw_exception("Indexing an empty vector variable", opt_key);
|
||||
const ConfigOption *opt_index = ctx->resolve_symbol(std::string(opt_vector_index.begin(), opt_vector_index.end()));
|
||||
if (opt_index == nullptr)
|
||||
ctx->throw_exception("Variable does not exist", opt_key);
|
||||
if (opt_index->type() != coInt)
|
||||
ctx->throw_exception("Indexing variable has to be integer", opt_key);
|
||||
int idx = opt_index->getInt();
|
||||
if (idx < 0)
|
||||
ctx->throw_exception("Negative vector index", opt_key);
|
||||
@@ -1021,6 +1030,13 @@ namespace client
|
||||
output.writable = true;
|
||||
}
|
||||
output.opt = opt;
|
||||
} else if (ctx->check_inactive_names()) {
|
||||
// Only check the name. Back tracking may resolve the same identifier twice, so there are no side effects.
|
||||
const std::string key{ opt_key.begin(), opt_key.end() };
|
||||
if (ctx->resolve_symbol(key) == nullptr && ctx->resolve_output_symbol(key) == nullptr &&
|
||||
ctx->inactive_local_variables.count(key) == 0 &&
|
||||
(ctx->context_data == nullptr || ctx->context_data->inactive_global_variables.count(key) == 0))
|
||||
ctx->throw_exception("Not a variable name (in an inactive branch)", opt_key);
|
||||
}
|
||||
output.it_range = opt_key;
|
||||
}
|
||||
@@ -1426,6 +1442,13 @@ namespace client
|
||||
out.opt = ctx->config_local.optptr(key);
|
||||
}
|
||||
out.name = std::move(key);
|
||||
} else if (ctx->check_inactive_names()) {
|
||||
// Declared in a branch that is not taken: the name still counts as defined for the names that follow.
|
||||
std::string key(it_range.begin(), it_range.end());
|
||||
if (global_variable && ctx->context_data != nullptr)
|
||||
ctx->context_data->inactive_global_variables.insert(std::move(key));
|
||||
else
|
||||
ctx->inactive_local_variables.insert(std::move(key));
|
||||
}
|
||||
out.it_range = it_range;
|
||||
}
|
||||
@@ -1660,8 +1683,8 @@ namespace client
|
||||
const OptWithPos &rhs)
|
||||
{
|
||||
if (ctx->skipping())
|
||||
// Skipping, continue parsing.
|
||||
return true;
|
||||
// Skipping, let conditional_expression parse the whole right hand side, which may continue after the variable reference.
|
||||
return false;
|
||||
|
||||
if (lhs.opt) {
|
||||
assert(lhs.opt->is_vector());
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "libslic3r.h"
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -24,8 +25,15 @@ public:
|
||||
// If defined, then this dictionary is used by the scripts to define user variables and persist them
|
||||
// between PlaceholderParser evaluations.
|
||||
std::unique_ptr<DynamicConfig> global_config;
|
||||
// Global variables declared in {if} branches that were not taken, see check_inactive_branches.
|
||||
std::set<std::string> inactive_global_variables;
|
||||
};
|
||||
|
||||
// Orca: when set, variable names inside {if} branches that are not taken must resolve too, so a single
|
||||
// expansion checks every branch of a template. Only the profile validator's slice sweep sets it.
|
||||
// It does not apply to evaluate_boolean_expression(), where an error reads as "compatible".
|
||||
static inline bool check_inactive_branches = false;
|
||||
|
||||
PlaceholderParser(const DynamicConfig *external_config = nullptr);
|
||||
|
||||
void clear_config() { m_config.clear(); }
|
||||
|
||||
@@ -318,10 +318,13 @@ Points Polygon::concave_points(double angle_threshold) const
|
||||
}
|
||||
|
||||
// Projection of a point onto the polygon.
|
||||
Point Polygon::point_projection(const Point &point) const
|
||||
Point Polygon::point_projection(const Point &point, size_t *edge_index) const
|
||||
{
|
||||
Point proj = point;
|
||||
double dmin = std::numeric_limits<double>::max();
|
||||
// Preserve the existing projection and tie order while optionally tracking its edge.
|
||||
if (edge_index)
|
||||
*edge_index = std::numeric_limits<size_t>::max();
|
||||
if (! this->points.empty()) {
|
||||
for (size_t i = 0; i < this->points.size(); ++ i) {
|
||||
const Point &pt0 = this->points[i];
|
||||
@@ -330,11 +333,15 @@ Point Polygon::point_projection(const Point &point) const
|
||||
if (d < dmin) {
|
||||
dmin = d;
|
||||
proj = pt0;
|
||||
if (edge_index)
|
||||
*edge_index = i;
|
||||
}
|
||||
d = (point - pt1).cast<double>().norm();
|
||||
if (d < dmin) {
|
||||
dmin = d;
|
||||
proj = pt1;
|
||||
if (edge_index)
|
||||
*edge_index = (i + 1) % this->points.size();
|
||||
}
|
||||
Vec2d v1(coordf_t(pt1(0) - pt0(0)), coordf_t(pt1(1) - pt0(1)));
|
||||
coordf_t div = v1.squaredNorm();
|
||||
@@ -347,6 +354,8 @@ Point Polygon::point_projection(const Point &point) const
|
||||
if (d < dmin) {
|
||||
dmin = d;
|
||||
proj = foot;
|
||||
if (edge_index)
|
||||
*edge_index = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,9 @@ public:
|
||||
Points convex_points(double angle_threshold = 0.) const;
|
||||
Points concave_points(double angle_threshold = 0.) const;
|
||||
// Projection of a point onto the polygon.
|
||||
Point point_projection(const Point &point) const;
|
||||
// Optional index: start of the closest edge, or the vertex itself for an endpoint.
|
||||
// Empty polygons return the query point and std::numeric_limits<size_t>::max() as the index.
|
||||
Point point_projection(const Point &point, size_t *edge_index = nullptr) const;
|
||||
std::vector<float> parameter_by_length() const;
|
||||
|
||||
//BBS
|
||||
|
||||
+286
-187
@@ -5,6 +5,8 @@
|
||||
#include "Preset.hpp"
|
||||
#include "PresetBundle.hpp"
|
||||
#include "AppConfig.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "ParallelResolve.hpp"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
@@ -506,10 +508,11 @@ void Preset::normalize(DynamicPrintConfig &config)
|
||||
handle_legacy_sla(config);
|
||||
}
|
||||
|
||||
std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config)
|
||||
std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config,
|
||||
const DynamicPrintConfig *added)
|
||||
{
|
||||
std::string incorrect_keys;
|
||||
for (const std::string &key : config.keys())
|
||||
for (const std::string &key : (added != nullptr ? *added : config).keys())
|
||||
if (! default_config.has(key)) {
|
||||
if (incorrect_keys.empty())
|
||||
incorrect_keys = key;
|
||||
@@ -621,7 +624,6 @@ void Preset::load_info(const std::string& file)
|
||||
}
|
||||
else if (v.first.compare("base_id") == 0) {
|
||||
this->base_id = v.second.get_value<std::string>();
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load info from: " << file << " and base_id: " << this->base_id;
|
||||
if (this->base_id.compare("null") == 0)
|
||||
this->base_id.clear();
|
||||
}
|
||||
@@ -974,6 +976,37 @@ void Preset::get_extruder_names_and_keysets(Type type, std::string& extruder_id_
|
||||
}
|
||||
}
|
||||
|
||||
DynamicPrintConfig Preset::load_external_config(Type type, const DynamicPrintConfig &default_config, const DynamicPrintConfig &project_config,
|
||||
const std::set<std::string> &different_settings_list,
|
||||
const std::function<DynamicPrintConfig *(const std::string &inherits)> &find_base,
|
||||
t_config_option_keys *keys)
|
||||
{
|
||||
// Load the preset over a default preset, so that the missing fields are filled in from the default preset.
|
||||
DynamicPrintConfig cfg(default_config);
|
||||
// SoftFever: ignore print connection info from project
|
||||
auto cfg_keys = cfg.keys();
|
||||
cfg_keys.erase(std::remove_if(cfg_keys.begin(), cfg_keys.end(),
|
||||
[](std::string &val) {
|
||||
return val == "print_host" || val == "print_host_webui" || val == "printhost_apikey" ||
|
||||
val == "printhost_cafile" || val == "printhost_user" || val == "printhost_password" || val == "printhost_port";
|
||||
}),
|
||||
cfg_keys.end());
|
||||
cfg.apply_only(project_config, cfg_keys, true);
|
||||
|
||||
//add different settings check logic, replace the old system preset's default value with new system preset's default values
|
||||
if (!different_settings_list.empty()) {
|
||||
if (DynamicPrintConfig *base_config = find_base(Preset::inherits(cfg))) {
|
||||
std::string extruder_id_name, extruder_variant_name;
|
||||
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
|
||||
Preset::get_extruder_names_and_keysets(type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
|
||||
cfg.update_non_diff_values_to_base_config(*base_config, cfg_keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||
}
|
||||
}
|
||||
if (keys)
|
||||
*keys = std::move(cfg_keys);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
bool Preset::has_lidar(PresetBundle *preset_bundle)
|
||||
{
|
||||
bool has_lidar = false;
|
||||
@@ -1669,6 +1702,169 @@ std::string PresetCollection::canonical_preset_name(const std::string &name, con
|
||||
return get_preset_canonical_name(parsed.bare, origin);
|
||||
}
|
||||
|
||||
PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
|
||||
const boost::filesystem::path &file, const std::string &canonical_name,
|
||||
const PresetOrigin &load_origin, ForwardCompatibilitySubstitutionRule substitution_rule,
|
||||
const std::string &extruder_id_name, const std::string &extruder_variant_name,
|
||||
std::set<std::string> *key_set1, std::set<std::string> *key_set2) const
|
||||
{
|
||||
UserPresetLoad out;
|
||||
out.preset = Preset(m_type, canonical_name, false);
|
||||
Preset &preset = out.preset;
|
||||
preset.bundle_id = load_origin.bundle_id;
|
||||
preset.file = file.string();
|
||||
// Load the preset file, apply preset values on top of defaults.
|
||||
try {
|
||||
fs::path idx_path(preset.file);
|
||||
idx_path.replace_extension(".info");
|
||||
if (fs::exists(idx_path)) {
|
||||
out.info_file = idx_path.string();
|
||||
preset.load_info(out.info_file);
|
||||
}
|
||||
DynamicPrintConfig config;
|
||||
//BBS: change to json format
|
||||
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
|
||||
std::map<std::string, std::string> key_values;
|
||||
std::string reason;
|
||||
ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
|
||||
if (! config_substitutions.empty())
|
||||
out.substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
|
||||
if (!reason.empty()) {
|
||||
out.discard_file = true;
|
||||
out.errors.push_back((boost::format("parse config %1% failed") % preset.file).str());
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
|
||||
boost::optional<Semver> version = Semver::parse(version_str);
|
||||
if (!version) return out;
|
||||
preset.version = *version;
|
||||
|
||||
if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end())
|
||||
preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID];
|
||||
if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end())
|
||||
preset.description = key_values[BBL_JSON_KEY_DESCRIPTION];
|
||||
if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end())
|
||||
preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false";
|
||||
|
||||
//Orca: find and use the inherit config as the base
|
||||
const Preset* inherit_preset = nullptr;
|
||||
ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS);
|
||||
|
||||
// check inherits_config
|
||||
if (inherits_config) {
|
||||
ConfigOptionString * option_str = dynamic_cast<ConfigOptionString *> (inherits_config);
|
||||
std::string inherits_value = option_str->value;
|
||||
// Orca: try to find if the parent preset has been renamed
|
||||
inherit_preset = this->find_preset2(inherits_value);
|
||||
Preset::normalize_inherits(config, inherit_preset);
|
||||
}
|
||||
const Preset& default_preset = this->default_preset_for(config);
|
||||
if (inherit_preset) {
|
||||
preset.config = inherit_preset->config;
|
||||
preset.filament_id = inherit_preset->filament_id;
|
||||
extend_default_config_length(config, false, {});
|
||||
preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||
}
|
||||
else {
|
||||
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config);
|
||||
if ((inherits_config2 && !inherits_config2->value.empty())) {
|
||||
out.errors.push_back((boost::format("can not find parent %1% for config %2%!") % inherits_config2->value % preset.file).str());
|
||||
return out;
|
||||
}
|
||||
// We support custom root preset now
|
||||
// Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field.
|
||||
preset.config = default_preset.config;
|
||||
preset.config.apply(std::move(config));
|
||||
extend_default_config_length(preset.config, true, default_preset.config);
|
||||
}
|
||||
|
||||
Preset::normalize(preset.config);
|
||||
// Report configuration fields, which are misplaced into a wrong group.
|
||||
std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config);
|
||||
if (!incorrect_keys.empty())
|
||||
out.errors.push_back("Error in a preset file: The preset \"" + preset.file +
|
||||
"\" contains the following incorrect keys: " + incorrect_keys + ", which were removed");
|
||||
|
||||
if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) {
|
||||
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset.config.option("compatible_printers", true));
|
||||
if (compatible_printers && compatible_printers->values.empty()) {
|
||||
size_t at_pos = canonical_name.find('@');
|
||||
if (at_pos != std::string::npos && at_pos + 1 < canonical_name.length()) {
|
||||
compatible_printers->values.push_back(canonical_name.substr(at_pos + 1));
|
||||
out.save_compatible_printers = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preset.loaded = true;
|
||||
out.complete = true;
|
||||
} catch (const std::ifstream::failure &err) {
|
||||
out.discard_file = true;
|
||||
out.errors.push_back((boost::format("The user-config cannot be loaded: %1%. Reason: %2%") % preset.file % err.what()).str());
|
||||
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
|
||||
} catch (const std::runtime_error &err) {
|
||||
out.discard_file = true;
|
||||
out.errors.push_back((boost::format("Failed loading the user-config file: %1%. Reason: %2%") % preset.file % err.what()).str());
|
||||
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
|
||||
}
|
||||
out.install = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
void PresetCollection::commit_user_preset(UserPresetLoad &&loaded, std::deque<Preset> &presets_loaded,
|
||||
PresetsConfigSubstitutions &substitutions,
|
||||
const std::function<void(Preset&)> &preset_loaded_fn,
|
||||
bool read_only)
|
||||
{
|
||||
Preset &preset = loaded.preset;
|
||||
if (! loaded.info_file.empty())
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load info from: " << loaded.info_file << " and base_id: " << preset.base_id;
|
||||
append(substitutions, std::move(loaded.substitutions));
|
||||
for (const std::string &error : loaded.errors) {
|
||||
++m_errors;
|
||||
BOOST_LOG_TRIVIAL(error) << error;
|
||||
}
|
||||
if (loaded.discard_file && !read_only) {
|
||||
fs::path file_path(loaded.preset.file);
|
||||
if (fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
file_path.replace_extension(".info");
|
||||
if (fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
}
|
||||
if (!loaded.install)
|
||||
return;
|
||||
|
||||
if (loaded.complete) {
|
||||
if (loaded.save_compatible_printers) {
|
||||
// A filesystem error from the rewrite is counted, and the preset still loads.
|
||||
try {
|
||||
if (!read_only)
|
||||
preset.save(nullptr);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << preset.name;
|
||||
} catch (const std::runtime_error &err) {
|
||||
++m_errors;
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " can not write compatible_printers back to " << preset.file << ": " << err.what();
|
||||
}
|
||||
}
|
||||
//BBS: add some workaround for previous incorrect settings
|
||||
if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id))
|
||||
preset.setting_id.clear();
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << preset.name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id;
|
||||
//BBS: add config related logs
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible;
|
||||
// add alias for custom filament preset
|
||||
set_custom_preset_alias(preset);
|
||||
}
|
||||
|
||||
if (preset_loaded_fn != nullptr)
|
||||
preset_loaded_fn(preset);
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name;
|
||||
presets_loaded.emplace_back(std::move(preset));
|
||||
}
|
||||
|
||||
// Load all presets found in dir_path.
|
||||
// Throws an exception on error.
|
||||
void PresetCollection::load_presets(
|
||||
@@ -1706,6 +1902,8 @@ void PresetCollection::load_presets(
|
||||
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
|
||||
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
|
||||
|
||||
struct UserPresetFile { fs::path path; std::string canonical_name; };
|
||||
std::vector<UserPresetFile> files;
|
||||
//BBS: change to json format
|
||||
for (auto &dir_entry : boost::filesystem::directory_iterator(dir))
|
||||
{
|
||||
@@ -1721,149 +1919,26 @@ void PresetCollection::load_presets(
|
||||
BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name;
|
||||
continue;
|
||||
}
|
||||
files.push_back({ dir_entry.path(), std::move(canonical_name) });
|
||||
}
|
||||
}
|
||||
|
||||
resolve_then_commit<CNumericLocalesSetter>(files.size(),
|
||||
[&](size_t i) {
|
||||
return this->resolve_user_preset(files[i].path, files[i].canonical_name, resolved_origin, substitution_rule,
|
||||
extruder_id_name, extruder_variant_name, key_set1, key_set2);
|
||||
},
|
||||
[&](size_t, UserPresetLoad &&loaded) {
|
||||
// Committing can remove an unreadable preset's file, and a filesystem error
|
||||
// there is reported without stopping the rest of the directory.
|
||||
try {
|
||||
Preset preset(m_type, canonical_name, false);
|
||||
preset.bundle_id = resolved_origin.bundle_id;
|
||||
preset.file = dir_entry.path().string();
|
||||
// Load the preset file, apply preset values on top of defaults.
|
||||
try {
|
||||
fs::path idx_path(preset.file);
|
||||
idx_path.replace_extension(".info");
|
||||
if (fs::exists(idx_path)) {
|
||||
preset.load_info(idx_path.string());
|
||||
}
|
||||
DynamicPrintConfig config;
|
||||
//BBS: change to json format
|
||||
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
|
||||
std::map<std::string, std::string> key_values;
|
||||
std::string reason;
|
||||
ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
|
||||
if (! config_substitutions.empty())
|
||||
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 (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
file_path.replace_extension(".info");
|
||||
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;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
|
||||
boost::optional<Semver> version = Semver::parse(version_str);
|
||||
if (!version) continue;
|
||||
preset.version = *version;
|
||||
|
||||
if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end())
|
||||
preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID];
|
||||
if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end())
|
||||
preset.description = key_values[BBL_JSON_KEY_DESCRIPTION];
|
||||
if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end())
|
||||
preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false";
|
||||
|
||||
//Orca: find and use the inherit config as the base
|
||||
Preset* inherit_preset = nullptr;
|
||||
ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS);
|
||||
|
||||
// check inherits_config
|
||||
if (inherits_config) {
|
||||
ConfigOptionString * option_str = dynamic_cast<ConfigOptionString *> (inherits_config);
|
||||
std::string inherits_value = option_str->value;
|
||||
// Orca: try to find if the parent preset has been renamed
|
||||
inherit_preset = this->find_preset2(inherits_value);
|
||||
Preset::normalize_inherits(config, inherit_preset);
|
||||
} else {
|
||||
;
|
||||
}
|
||||
const Preset& default_preset = this->default_preset_for(config);
|
||||
if (inherit_preset) {
|
||||
preset.config = inherit_preset->config;
|
||||
preset.filament_id = inherit_preset->filament_id;
|
||||
extend_default_config_length(config, false, {});
|
||||
preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||
}
|
||||
else {
|
||||
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config);
|
||||
if ((inherits_config2 && !inherits_config2->value.empty())) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent %1% for config %2%!")%inherits_config2->value %preset.file;
|
||||
++m_errors;
|
||||
continue;
|
||||
}
|
||||
// We support custom root preset now
|
||||
// Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field.
|
||||
preset.config = default_preset.config;
|
||||
preset.config.apply(std::move(config));
|
||||
extend_default_config_length(preset.config, true, default_preset.config);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id;
|
||||
|
||||
Preset::normalize(preset.config);
|
||||
// Report configuration fields, which are misplaced into a wrong group.
|
||||
std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config);
|
||||
if (!incorrect_keys.empty()) {
|
||||
++m_errors;
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< "Error in a preset file: The preset \"" << preset.file
|
||||
<< "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed";
|
||||
}
|
||||
|
||||
if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) {
|
||||
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset.config.option("compatible_printers", true));
|
||||
if (compatible_printers && compatible_printers->values.empty()) {
|
||||
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));
|
||||
if (!read_only)
|
||||
preset.save(nullptr);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preset.loaded = true;
|
||||
//BBS: add some workaround for previous incorrect settings
|
||||
if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id))
|
||||
preset.setting_id.clear();
|
||||
//BBS: add config related logs
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible;
|
||||
// add alias for custom filament preset
|
||||
set_custom_preset_alias(preset);
|
||||
} catch (const std::ifstream::failure &err) {
|
||||
++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 (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
file_path.replace_extension(".info");
|
||||
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) {
|
||||
++m_errors;
|
||||
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 (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
file_path.replace_extension(".info");
|
||||
if (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
}
|
||||
|
||||
if (preset_loaded_fn != nullptr)
|
||||
preset_loaded_fn(preset);
|
||||
|
||||
presets_loaded.emplace_back(preset);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name;
|
||||
this->commit_user_preset(std::move(loaded), presets_loaded, substitutions, preset_loaded_fn, read_only);
|
||||
} catch (const std::runtime_error &err) {
|
||||
errors_cummulative += err.what();
|
||||
errors_cummulative += "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (presets_loaded.size() > 0)
|
||||
m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end()));
|
||||
sort_presets();
|
||||
@@ -2596,20 +2671,6 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
|
||||
const Semver file_version,
|
||||
const std::string filament_id)
|
||||
{
|
||||
// Load the preset over a default preset, so that the missing fields are filled in from the default preset.
|
||||
DynamicPrintConfig cfg(this->default_preset_for(combined_config).config);
|
||||
// SoftFever: ignore print connection info from project
|
||||
auto keys = cfg.keys();
|
||||
keys.erase(std::remove_if(keys.begin(), keys.end(),
|
||||
[](std::string &val) {
|
||||
return val == "print_host" || val == "print_host_webui" || val == "printhost_apikey" ||
|
||||
val == "printhost_cafile" || val == "printhost_user" || val == "printhost_password" || val == "printhost_port";
|
||||
}),
|
||||
keys.end());
|
||||
cfg.apply_only(combined_config, keys, true);
|
||||
std::string &inherits = Preset::inherits(cfg);
|
||||
|
||||
//add different settings check logic, replace the old system preset's default value with new system preset's default values
|
||||
std::deque<Preset>::iterator it = this->find_preset_internal(original_name);
|
||||
bool found = it != m_presets.end() && it->name == original_name;
|
||||
if (! found) {
|
||||
@@ -2618,24 +2679,25 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
|
||||
found = it != m_presets.end();
|
||||
}
|
||||
|
||||
std::string extruder_id_name, extruder_variant_name;
|
||||
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
|
||||
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
|
||||
|
||||
if (!inherits.empty() && (different_settings_list.size() > 0)) {
|
||||
auto iter = this->find_preset_internal(inherits);
|
||||
if (iter == m_presets.end() || iter->name != inherits)
|
||||
iter = this->find_preset_renamed(inherits);
|
||||
if (iter != m_presets.end()) {
|
||||
//std::vector<std::string> dirty_options = cfg.diff(iter->config);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": change preset %1% inherit %2% 's value to %3% 's values")%original_name %inherits %path;
|
||||
cfg.update_non_diff_values_to_base_config(iter->config, keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||
}
|
||||
}
|
||||
else if (found && it->is_system && (different_settings_list.size() > 0)) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": change preset %1% 's value to %2% 's values")%original_name %path;
|
||||
cfg.update_non_diff_values_to_base_config(it->config, keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||
}
|
||||
t_config_option_keys keys;
|
||||
DynamicPrintConfig cfg = Preset::load_external_config(m_type, this->default_preset_for(combined_config).config, combined_config, different_settings_list,
|
||||
[this, &original_name, &path, found, it](const std::string &inherits) -> DynamicPrintConfig * {
|
||||
if (!inherits.empty()) {
|
||||
auto iter = this->find_preset_internal(inherits);
|
||||
if (iter == m_presets.end() || iter->name != inherits)
|
||||
iter = this->find_preset_renamed(inherits);
|
||||
if (iter == m_presets.end())
|
||||
return nullptr;
|
||||
BOOST_LOG_TRIVIAL(info) << "load_external_preset" << boost::format(": change preset %1% inherit %2% 's value to %3% 's values")%original_name %inherits %path;
|
||||
return &iter->config;
|
||||
}
|
||||
if (found && it->is_system) {
|
||||
BOOST_LOG_TRIVIAL(info) << "load_external_preset" << boost::format(": change preset %1% 's value to %2% 's values")%original_name %path;
|
||||
return &it->config;
|
||||
}
|
||||
return nullptr;
|
||||
}, &keys);
|
||||
std::string &inherits = Preset::inherits(cfg);
|
||||
|
||||
//BBS: add config related logs
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, type %1% , path %2%, name %3%, original_name %4%, inherits %5%")%Preset::get_type_string(m_type) %path %name %original_name %inherits;
|
||||
@@ -2827,6 +2889,18 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
|
||||
return std::make_pair(&preset, false);
|
||||
}
|
||||
|
||||
Preset& PresetCollection::append_preset(std::string &&path, const std::string &name, DynamicPrintConfig &&config)
|
||||
{
|
||||
lock();
|
||||
Preset &preset = m_presets.emplace_back(m_type, name, false);
|
||||
preset.file = std::move(path);
|
||||
preset.config = std::move(config);
|
||||
preset.loaded = true;
|
||||
preset.is_dirty = false;
|
||||
unlock();
|
||||
return preset;
|
||||
}
|
||||
|
||||
Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select, Semver file_version)
|
||||
{
|
||||
lock();
|
||||
@@ -3846,28 +3920,53 @@ bool PresetCollection::select_preset_by_name_strict(const std::string &name)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
||||
std::vector<std::string> PresetCollection::merge_presets(PresetCollection &&other, const VendorMap &new_vendors)
|
||||
std::vector<std::vector<std::string>> PresetCollection::merge_presets(const std::vector<PresetCollection*> &others, const VendorMap &new_vendors)
|
||||
{
|
||||
std::vector<std::string> duplicates;
|
||||
for (Preset &preset : other.m_presets) {
|
||||
if (preset.is_default || preset.is_external)
|
||||
continue;
|
||||
Preset key(m_type, preset.name);
|
||||
auto it = (m_type == Preset::TYPE_FILAMENT)
|
||||
? std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key, filament_preset_less)
|
||||
: std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key);
|
||||
if (it == m_presets.end() || it->name != preset.name) {
|
||||
auto less = [this](const Preset &a, const Preset &b) {
|
||||
return m_type == Preset::TYPE_FILAMENT ? filament_preset_less(a, b) : a < b;
|
||||
};
|
||||
struct Incoming { Preset *preset; size_t source; };
|
||||
auto incoming_less = [&less](const Incoming &a, const Incoming &b) { return less(*a.preset, *b.preset); };
|
||||
// Each of `others` is sorted, so its presets form one sorted run.
|
||||
std::vector<Incoming> incoming;
|
||||
std::vector<size_t> run_ends { 0 };
|
||||
for (size_t source = 0; source < others.size(); ++ source) {
|
||||
for (Preset &preset : others[source]->m_presets)
|
||||
if (! preset.is_default && ! preset.is_external)
|
||||
incoming.push_back({ &preset, source });
|
||||
assert(std::is_sorted(incoming.begin() + run_ends.back(), incoming.end(), incoming_less));
|
||||
run_ends.push_back(incoming.size());
|
||||
}
|
||||
// Merged pairwise and stably, so equal names stay in the order of `others`.
|
||||
const size_t runs = others.size();
|
||||
for (size_t width = 1; width < runs; width *= 2)
|
||||
for (size_t i = 0; i + width < runs; i += 2 * width)
|
||||
std::inplace_merge(incoming.begin() + run_ends[i], incoming.begin() + run_ends[i + width],
|
||||
incoming.begin() + run_ends[std::min(i + 2 * width, runs)], incoming_less);
|
||||
|
||||
std::vector<std::vector<std::string>> duplicates(others.size());
|
||||
std::deque<Preset> merged;
|
||||
auto own = m_presets.begin() + m_num_default_presets;
|
||||
std::move(m_presets.begin(), own, std::back_inserter(merged));
|
||||
// On equal names this collection's preset is kept, else the earliest of `others`,
|
||||
// and each repeat is listed under the collection it came from.
|
||||
for (auto next = incoming.begin(); own != m_presets.end() || next != incoming.end();) {
|
||||
if (next == incoming.end() || (own != m_presets.end() && ! less(*next->preset, *own)))
|
||||
merged.emplace_back(std::move(*own ++));
|
||||
else {
|
||||
Preset &preset = *(next ++)->preset;
|
||||
if (preset.vendor != nullptr) {
|
||||
// Re-assign a pointer to the vendor structure in the new PresetBundle.
|
||||
auto it = new_vendors.find(preset.vendor->id);
|
||||
assert(it != new_vendors.end());
|
||||
preset.vendor = &it->second;
|
||||
}
|
||||
m_presets.emplace(it, std::move(preset));
|
||||
} else
|
||||
duplicates.emplace_back(std::move(preset.name));
|
||||
merged.emplace_back(std::move(preset));
|
||||
}
|
||||
for (; next != incoming.end() && next->preset->name == merged.back().name; ++ next)
|
||||
duplicates[next->source].emplace_back(next->preset->name);
|
||||
}
|
||||
m_presets = std::move(merged);
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
|
||||
@@ -403,6 +403,15 @@ public:
|
||||
std::string get_current_printer_type(PresetBundle *preset_bundle); // get current preset type
|
||||
|
||||
static void get_extruder_names_and_keysets(Type type, std::string& extruder_id_name, std::string& extruder_variant_name, std::set<std::string>** p_key_set1, std::set<std::string>** p_key_set2);
|
||||
// Config of a preset loaded from a project or config file: the project's values over the type's
|
||||
// default preset config, without the print-host keys. When different_settings_list is not empty,
|
||||
// every key not listed in it is then refreshed from the base system preset, which find_base returns
|
||||
// for the project's "inherits" (nullptr when there is none), with the listed per-variant values
|
||||
// mapped onto the base's extruder variants. keys, if given, receives the keys taken from the project.
|
||||
static DynamicPrintConfig load_external_config(Type type, const DynamicPrintConfig &default_config, const DynamicPrintConfig &project_config,
|
||||
const std::set<std::string> &different_settings_list,
|
||||
const std::function<DynamicPrintConfig *(const std::string &inherits)> &find_base,
|
||||
t_config_option_keys *keys = nullptr);
|
||||
std::string get_printer_id() const { return vendor ? vendor->id : ""; }
|
||||
|
||||
bool has_lidar(PresetBundle *preset_bundle);
|
||||
@@ -445,7 +454,10 @@ public:
|
||||
static std::string remove_suffix_modified(const std::string& name);
|
||||
static void normalize(DynamicPrintConfig &config);
|
||||
// Report configuration fields, which are misplaced into a wrong group, remove them from the config.
|
||||
static std::string remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config);
|
||||
// `added`, when given, is the diff applied over a copy of default_config, and only
|
||||
// its keys are checked, since no other key can be missing from default_config.
|
||||
static std::string remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config,
|
||||
const DynamicPrintConfig *added = nullptr);
|
||||
|
||||
// BBS: move constructor to public
|
||||
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
|
||||
@@ -801,6 +813,8 @@ public:
|
||||
// Return number of presets including the "- default -" preset.
|
||||
size_t size() const { return m_presets.size(); }
|
||||
bool has_defaults_only() const { return m_presets.size() <= m_num_default_presets; }
|
||||
// How many presets this collection refused or repaired while loading.
|
||||
int error_count() const { return m_errors; }
|
||||
|
||||
// For Print / Filament presets, disable those, which are not compatible with the printer.
|
||||
template<typename PreferedCondition>
|
||||
@@ -874,8 +888,10 @@ protected:
|
||||
// This is a temporary state, which shall be fixed immediately by the following step.
|
||||
bool select_preset_by_name_strict(const std::string &name);
|
||||
|
||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
||||
std::vector<std::string> merge_presets(PresetCollection &&other, const VendorMap &new_vendors);
|
||||
// Move the presets of `others` into this collection in one pass. A name this
|
||||
// collection or an earlier one of `others` already has is left out, and reported
|
||||
// in the list of the collection that repeats it.
|
||||
std::vector<std::vector<std::string>> merge_presets(const std::vector<PresetCollection*> &others, const VendorMap &new_vendors);
|
||||
|
||||
// Update m_map_alias_to_profile_name from loaded system profiles.
|
||||
void update_map_alias_to_profile_name();
|
||||
@@ -890,6 +906,43 @@ protected:
|
||||
void set_custom_preset_alias(Preset &preset);
|
||||
|
||||
private:
|
||||
// One preset file read and flattened against the presets already in this
|
||||
// collection, before anything the collection shares has been touched.
|
||||
struct UserPresetLoad
|
||||
{
|
||||
Preset preset;
|
||||
// Joins the collection. A file that threw partway still joins it, without
|
||||
// the steps that did not run.
|
||||
bool install { false };
|
||||
// The whole of the load ran, so the preset is ready to be aliased.
|
||||
bool complete { false };
|
||||
// A filament preset that named no compatible printer and was given one from
|
||||
// its name, which commit writes back to its file.
|
||||
bool save_compatible_printers { false };
|
||||
// Unreadable, so commit removes it and its .info file.
|
||||
bool discard_file { false };
|
||||
// The .info file read beside the preset, which commit logs.
|
||||
std::string info_file;
|
||||
// Counted and logged by commit, in the order the directory listed the files.
|
||||
std::vector<std::string> errors;
|
||||
PresetsConfigSubstitutions substitutions;
|
||||
};
|
||||
|
||||
// Read and flatten one preset file. It reads only, and resolves against the presets
|
||||
// loaded before this pass, never another file of the same pass, so the files of a
|
||||
// pass are independent of each other.
|
||||
UserPresetLoad resolve_user_preset(const boost::filesystem::path &file, const std::string &canonical_name,
|
||||
const PresetOrigin &load_origin, ForwardCompatibilitySubstitutionRule substitution_rule,
|
||||
const std::string &extruder_id_name, const std::string &extruder_variant_name,
|
||||
std::set<std::string> *key_set1, std::set<std::string> *key_set2) const;
|
||||
|
||||
// Install one resolved preset. The collection, its alias maps, the error count
|
||||
// and the preset files on disk are touched here and only here.
|
||||
void commit_user_preset(UserPresetLoad &&loaded, std::deque<Preset> &presets_loaded,
|
||||
PresetsConfigSubstitutions &substitutions,
|
||||
const std::function<void(Preset&)> &preset_loaded_fn,
|
||||
bool read_only);
|
||||
|
||||
std::string canonical_preset_name(const std::string &name, const PresetOrigin &load_origin = PresetOrigin()) const;
|
||||
|
||||
// Comparator that sorts "Generic " prefixed presets before others, then alphabetically within each group.
|
||||
@@ -901,6 +954,10 @@ private:
|
||||
return a.name < b.name;
|
||||
}
|
||||
|
||||
// Append a preset without keeping the collection sorted, for a caller installing
|
||||
// many at once; find_preset() is unusable until sort_presets() runs.
|
||||
Preset& append_preset(std::string &&path, const std::string &name, DynamicPrintConfig &&config);
|
||||
|
||||
// Sort presets: filament presets use generic-first ordering, others sort alphabetically.
|
||||
void sort_presets() {
|
||||
if (m_type == Preset::TYPE_FILAMENT)
|
||||
|
||||
+624
-435
File diff suppressed because it is too large
Load Diff
+182
-29
@@ -15,6 +15,7 @@
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <unordered_set>
|
||||
|
||||
@@ -234,6 +235,9 @@ public:
|
||||
|
||||
// ORCA: utility function to find the vendor for a given preset name
|
||||
static std::string find_preset_vendor(const std::string& preset_name, Preset::Type type);
|
||||
// Keys a project keeps when its presets are loaded: those listed in its escaped
|
||||
// "different_settings_to_system" entry for the preset, plus the preset bookkeeping keys.
|
||||
static std::set<std::string> project_different_keys(const std::string &different_settings);
|
||||
|
||||
PresetBundle();
|
||||
PresetBundle(const PresetBundle &rhs);
|
||||
@@ -273,6 +277,10 @@ public:
|
||||
const std::string &source_file,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
||||
std::string &error, bool allow_source_manifest = true);
|
||||
// Resolve a system preset by name. The vendor tree is read from data_dir()/system when installed
|
||||
// there, as the GUI reads it, and from the bundled profiles otherwise.
|
||||
bool resolve_system_preset(DynamicPrintConfig &config, Preset::Type type, const std::string &name,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule, std::string &error);
|
||||
|
||||
// Load selections (current print, current filaments, current printer) from config.ini
|
||||
// This is done just once on application start up.
|
||||
@@ -622,36 +630,180 @@ public:
|
||||
// default_filament_profile must resolve to a system filament.
|
||||
bool check_printer_default_materials() const;
|
||||
|
||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
||||
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
|
||||
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
|
||||
std::vector<std::string> merge_presets(PresetBundle &&other);
|
||||
// One vendor to load, and the directory it is installed in.
|
||||
struct VendorSource
|
||||
{
|
||||
std::string name;
|
||||
boost::filesystem::path dir;
|
||||
};
|
||||
|
||||
// Load `vendors` into this bundle, the Orca filament library directly and every
|
||||
// other vendor in parallel into a bundle of its own that inherits from it, merged
|
||||
// in the order given. A vendor that cannot be loaded has its error added to the
|
||||
// returned text, or thrown in validation mode, and its name to `failed`; it is
|
||||
// left out, except for the library, which keeps what it installed before the
|
||||
// failure. Once `cancel` is set, no further vendor starts loading.
|
||||
std::pair<PresetsConfigSubstitutions, std::string> load_vendors(const std::vector<VendorSource>& vendors,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache,
|
||||
const std::atomic<bool>* cancel = nullptr, std::vector<std::string>* failed = nullptr);
|
||||
|
||||
private:
|
||||
// Load one vendor from the preset cache installed in `dir`, judged against
|
||||
// the vendor profile there. False, with this bundle left clean, when there
|
||||
// is no usable cache and the vendor has to be parsed. This is how
|
||||
// load_vendor_configs_from_json reads a cache.
|
||||
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
|
||||
// Move the presets and vendor profiles of `others` into this bundle, in one pass
|
||||
// over each collection. A preset whose name this bundle or an earlier one of
|
||||
// `others` already has is left out and listed under the bundle that repeats it.
|
||||
std::vector<std::vector<std::string>> merge_presets(const std::vector<PresetBundle*> &others);
|
||||
|
||||
// Load one source-form preset entry into this bundle: resolve `inherits`
|
||||
// and `include`, flatten, validate and register the preset. Returns the
|
||||
// reason loading failed, empty on success. See the definition for the
|
||||
// sharing contract between the JSON parse and the cache load.
|
||||
// retain_configs / retain_includes, when non-null, name the only presets
|
||||
// registered into config_maps / include_maps (a config copy each). The
|
||||
// cache load passes the names its entries inherit / include — the only
|
||||
// ones ever looked up again; the JSON parse retains all, not knowing what
|
||||
// later subfiles name.
|
||||
std::string load_vendor_preset(const CachedPreset& entry,
|
||||
const std::string& path, const std::string& vendor_name,
|
||||
const PresetBundle* base_bundle,
|
||||
LoadConfigBundleAttributes flags,
|
||||
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
|
||||
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, DynamicPrintConfig>& include_maps,
|
||||
std::map<std::string, std::string>& filament_id_maps,
|
||||
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
|
||||
const std::set<std::string>* retain_configs = nullptr, const std::set<std::string>* retain_includes = nullptr);
|
||||
// What parsing one entry's JSON sub-file reported. Its errors and warnings are
|
||||
// logged when the entry installs, so they come out in listing order with the
|
||||
// entry's install errors, as parsing and installing one entry at a time leaves them.
|
||||
struct EntryParse
|
||||
{
|
||||
ConfigSubstitutions substitutions;
|
||||
// Counted in the bundle's error count.
|
||||
std::vector<std::string> errors;
|
||||
std::vector<std::string> warnings;
|
||||
};
|
||||
|
||||
// An EntryParse for each entry of the VendorCacheData list of the same name.
|
||||
struct VendorParse
|
||||
{
|
||||
std::vector<EntryParse> process_entries;
|
||||
std::vector<EntryParse> filament_entries;
|
||||
std::vector<EntryParse> machine_entries;
|
||||
};
|
||||
|
||||
// One vendor read from its cache or its JSONs by read_vendor, for
|
||||
// install_vendor_read to install.
|
||||
struct VendorRead
|
||||
{
|
||||
std::string dir;
|
||||
std::string vendor_name;
|
||||
LoadConfigBundleAttributes flags;
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule;
|
||||
// The errors this bundle had counted before the read, which the cache stamp leaves out.
|
||||
int errors_at_entry { 0 };
|
||||
// A whole-vendor load, which can read a cache and write one.
|
||||
bool cacheable { false };
|
||||
// Read from the cache at cache_path, which install can still reject.
|
||||
bool from_cache { false };
|
||||
std::string cache_path;
|
||||
// Only the vendor profile was asked for.
|
||||
bool vendor_only { false };
|
||||
VendorCacheData data;
|
||||
// What each entry's JSON parse reported, and whether and with which version
|
||||
// the cache is written once the entries install.
|
||||
VendorParse parsed;
|
||||
bool will_cache { false };
|
||||
std::string version;
|
||||
// The sub-file the parse stopped at, its kind, why, and the errors it reported.
|
||||
std::string reason;
|
||||
std::string failed_subfile;
|
||||
const char* failed_kind { nullptr };
|
||||
std::vector<std::string> failed_errors;
|
||||
};
|
||||
|
||||
// Read a vendor into this bundle's vendor profiles and `data`, from its cache
|
||||
// when one covers it, else from its JSONs; nothing is installed. Throws
|
||||
// ConfigurationError when the vendor's own JSON cannot be parsed.
|
||||
VendorRead read_vendor(const std::string& dir, const std::string& vendor_name, LoadConfigBundleAttributes flags,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache);
|
||||
|
||||
// Parse the vendor's JSONs into `read`, up to the first sub-file that fails.
|
||||
void parse_vendor_json(VendorRead& read);
|
||||
|
||||
// Install what read_vendor read, against base_bundle's filament library. A cache
|
||||
// that cannot be installed is replaced by a parse of the JSONs. Throws
|
||||
// ConfigurationError at the first entry that cannot be installed, or after
|
||||
// installing the entries before a sub-file that could not be parsed.
|
||||
std::pair<PresetsConfigSubstitutions, size_t> install_vendor_read(VendorRead&& read, const PresetBundle* base_bundle);
|
||||
|
||||
// Install a cache's entries. False, with this bundle left clean, when one of them
|
||||
// cannot be installed.
|
||||
bool install_vendor_cache(const std::string& cache_path, const std::string& vendor_name, VendorCacheData&& data,
|
||||
const PresetBundle* base_bundle);
|
||||
|
||||
// Log and count errors reported by a resolve or a parse.
|
||||
void log_errors(const std::vector<std::string>& errors);
|
||||
|
||||
// The state of installing one collection of one vendor, which
|
||||
// resolve_vendor_preset reads through a const reference and only
|
||||
// commit_vendor_preset writes.
|
||||
struct VendorInstall
|
||||
{
|
||||
// The directory holding <vendor_name>/, whose sub-paths the entries name.
|
||||
std::string path;
|
||||
std::string vendor_name;
|
||||
const VendorProfile* vendor_profile;
|
||||
const PresetBundle* base_bundle;
|
||||
LoadConfigBundleAttributes flags;
|
||||
PresetCollection* presets;
|
||||
// The Orca filament library, which keeps every config for other vendors to
|
||||
// resolve against.
|
||||
bool is_from_lib;
|
||||
PresetsConfigSubstitutions* substitutions;
|
||||
// The names some entry inherits / includes, the only ones whose configs /
|
||||
// include diffs are looked up again.
|
||||
std::set<std::string> inherited;
|
||||
std::set<std::string> included;
|
||||
std::map<std::string, DynamicPrintConfig> config_maps;
|
||||
std::map<std::string, DynamicPrintConfig> include_maps;
|
||||
std::map<std::string, std::string> filament_id_maps;
|
||||
std::unordered_set<std::string> installed_names;
|
||||
size_t count { 0 };
|
||||
};
|
||||
|
||||
// Install a vendor's source-form entries, parsed from its JSON or read from its
|
||||
// cache: processes, then filaments, then printers. Both loads go through here,
|
||||
// so a cache-loaded bundle cannot come out different from a JSON-loaded one.
|
||||
// `parsed` is given for entries parsed just now. `complete` says the entries
|
||||
// are the vendor's whole lists; only then are the filament library's configs
|
||||
// and filament ids left in m_config_maps and m_filament_id_maps. Returns the
|
||||
// number of presets installed, and throws ConfigurationError at the first
|
||||
// entry that cannot be installed.
|
||||
size_t install_vendor(const std::string& path, const std::string& vendor_name, const PresetBundle* base_bundle,
|
||||
LoadConfigBundleAttributes flags, const VendorCacheData& entries,
|
||||
VendorParse* parsed, bool complete, PresetsConfigSubstitutions& substitutions);
|
||||
|
||||
// Install one collection's entries in the order they are listed.
|
||||
void install_vendor_entries(VendorInstall& install, const std::vector<CachedPreset>& entries,
|
||||
std::vector<EntryParse>* parsed);
|
||||
|
||||
// One entry flattened against the preset it inherits, before anything this
|
||||
// bundle shares has been touched.
|
||||
struct PresetInstall
|
||||
{
|
||||
DynamicPrintConfig config;
|
||||
std::string file_path;
|
||||
// Empty when the preset is its own alias.
|
||||
std::string alias;
|
||||
std::string filament_id;
|
||||
std::vector<std::string> renamed_from;
|
||||
// Reported by commit, so resolving entries together leaves the log and
|
||||
// the error count as one entry at a time produces them.
|
||||
std::vector<std::string> errors;
|
||||
// What a base states for the presets that include it, when it is retained.
|
||||
std::optional<DynamicPrintConfig> included;
|
||||
// The config kept for the entries that inherit this one, or for other
|
||||
// vendors when this is the filament library.
|
||||
std::optional<DynamicPrintConfig> retained;
|
||||
// Not instantiated, so it contributes a config and no preset.
|
||||
bool config_only { false };
|
||||
// Non-empty when the entry is rejected, and says why.
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
// Flatten one entry against the config it inherits, from this collection's
|
||||
// config_maps or base_bundle's filament library, with the include diffs it
|
||||
// names layered in. It looks up nothing but the names the entry inherits and
|
||||
// includes, and writes nothing.
|
||||
PresetInstall resolve_vendor_preset(const CachedPreset& entry, const VendorInstall& install) const;
|
||||
|
||||
// Install a resolved entry. The collections, the maps in `install` and the
|
||||
// error count are touched here and only here, one entry at a time.
|
||||
// Presets are appended, so a repeated name is caught with `installed_names`,
|
||||
// and the collection is sorted once every entry is in.
|
||||
std::string commit_vendor_preset(const CachedPreset& entry, PresetInstall&& resolved,
|
||||
ConfigSubstitutions&& substitutions, VendorInstall& install);
|
||||
|
||||
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
|
||||
void clear_printer_hold_aliases();
|
||||
@@ -663,13 +815,14 @@ private:
|
||||
// Vendor trees loaded by resolve_preset_config's manifest path, so every preset
|
||||
// resolved through this bundle shares one load per source root and vendor. The
|
||||
// filament library is one such tree, shared by every vendor under its root.
|
||||
std::map<std::tuple<std::string, std::string, ForwardCompatibilitySubstitutionRule>, std::unique_ptr<PresetBundle>>
|
||||
// A tree read from its preset cache is kept apart: its presets carry no source file.
|
||||
std::map<std::tuple<std::string, std::string, ForwardCompatibilitySubstitutionRule, bool>, std::unique_ptr<PresetBundle>>
|
||||
m_source_vendor_bundles;
|
||||
|
||||
const PresetBundle *load_source_vendor(const boost::filesystem::path &root_dir,
|
||||
const std::string &vendor_id,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
||||
std::string &error);
|
||||
std::string &error, bool allow_cache = false);
|
||||
|
||||
// Orca: validation only - flag any printer with two or more compatible
|
||||
// filament presets sharing one filament_id (ambiguous AMS subtype match).
|
||||
|
||||
@@ -109,7 +109,7 @@ void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict);
|
||||
|
||||
// One preset as its JSON subfile states it: the config diff, the names of the
|
||||
// preset it inherits and the presets it includes, and the parse metadata —
|
||||
// everything the parse phase of load_vendor_configs_from_json extracts and
|
||||
// everything PresetBundle::parse_vendor_json extracts and
|
||||
// nothing it derives. Inheritance and includes are resolved when the entry is
|
||||
// installed, against whatever filament library is loaded then, so a cache
|
||||
// carries no other vendor's values and no other vendor's update can make it
|
||||
|
||||
+19
-9
@@ -2439,12 +2439,18 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
if (m_default_region_config.precise_outer_wall && m_default_region_config.wall_sequence != WallSequence::InnerOuter)
|
||||
warn(L("The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences."), "precise_outer_wall");
|
||||
|
||||
// check adaptive pressure advance model
|
||||
for (unsigned int extruder_id : extruders) {
|
||||
if (m_config.adaptive_pressure_advance.get_at(extruder_id) &&
|
||||
m_config.enable_pressure_advance.get_at(extruder_id)) {
|
||||
|
||||
const std::string pa_model = m_config.adaptive_pressure_advance_model.get_at(extruder_id);
|
||||
// check adaptive pressure advance model of every extruder variant column of the used filaments
|
||||
const std::vector<int> &self_index = m_config.filament_self_index.values;
|
||||
const size_t pa_columns = std::max(m_config.adaptive_pressure_advance_model.size(), size_t(extruders.back()) + 1);
|
||||
for (size_t column = 0; column < pa_columns; ++column) {
|
||||
// filament_self_index maps a column to its filament once the filament arrays hold one column per variant
|
||||
const unsigned int filament_id = self_index.size() == pa_columns ? self_index[column] - 1 : column;
|
||||
if (!std::binary_search(extruders.begin(), extruders.end(), filament_id))
|
||||
continue;
|
||||
if (m_config.adaptive_pressure_advance.get_at(column) &&
|
||||
m_config.enable_pressure_advance.get_at(column)) {
|
||||
|
||||
const std::string pa_model = m_config.adaptive_pressure_advance_model.get_at(column);
|
||||
if (!pa_model.empty()) {
|
||||
std::string validation_error = AdaptivePAProcessor::validate_adaptive_pa_model(pa_model);
|
||||
if (!validation_error.empty()) {
|
||||
@@ -3938,6 +3944,9 @@ void Print::update_filament_maps_to_config(std::vector<int> f_maps, std::vector<
|
||||
{
|
||||
int extruder_count = 1, extruder_volume_type_count = 1;
|
||||
bool support_multi = m_ori_full_print_config.support_different_extruders(extruder_count);
|
||||
// Orca: resolve the filament variants wherever Print::apply does, a multi-variant filament
|
||||
// on a single-variant printer included.
|
||||
const bool expand_filaments = (extruder_count > 1) || support_multi || m_ori_full_print_config.has_multi_variant_filament();
|
||||
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
|
||||
extruder_volume_type_count = m_ori_full_print_config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
|
||||
|
||||
@@ -3978,7 +3987,7 @@ void Print::update_filament_maps_to_config(std::vector<int> f_maps, std::vector<
|
||||
m_full_print_config = m_ori_full_print_config;
|
||||
std::set<std::string> filament_keys = filament_options_with_variant;
|
||||
filament_keys.insert("filament_self_index");
|
||||
if ((extruder_count > 1) || support_multi)
|
||||
if (expand_filaments)
|
||||
m_full_print_config.update_values_to_printer_extruders_for_multiple_filaments(m_full_print_config, extruder_count, extruder_volume_type_count, filament_keys, "filament_self_index", "filament_extruder_variant");
|
||||
|
||||
const std::vector<std::string> &extruder_retract_keys = print_config_def.extruder_retract_keys();
|
||||
@@ -3995,7 +4004,7 @@ void Print::update_filament_maps_to_config(std::vector<int> f_maps, std::vector<
|
||||
compute_filament_override_value(opt_key, opt_old_machine, opt_new_machine, opt_new_filament, m_full_print_config, print_diff, filament_overrides, m_config.filament_map_2.values);
|
||||
}
|
||||
|
||||
if ((extruder_count > 1) || support_multi) {
|
||||
if (expand_filaments) {
|
||||
t_config_option_keys keys(filament_options_with_variant.begin(), filament_options_with_variant.end());
|
||||
keys.push_back("filament_self_index");
|
||||
m_config.apply_only(m_full_print_config, keys, true);
|
||||
@@ -5049,6 +5058,7 @@ DynamicConfig PrintStatistics::config() const
|
||||
config.set_key_value("total_wipe_tower_filament", new ConfigOptionFloat(this->total_wipe_tower_filament));
|
||||
config.set_key_value("initial_tool", new ConfigOptionInt(static_cast<int>(this->initial_tool)));
|
||||
config.set_key_value("initial_extruder", new ConfigOptionInt(static_cast<int>(this->initial_tool)));
|
||||
config.set_key_value("initial_no_support_extruder", new ConfigOptionInt(static_cast<int>(this->initial_no_support_tool)));
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -5058,7 +5068,7 @@ DynamicConfig PrintStatistics::placeholders()
|
||||
for (const std::string key : {
|
||||
"print_time", "normal_print_time", "silent_print_time",
|
||||
"used_filament", "extruded_volume", "extruded_volume_total", "total_cost", "total_weight", "extruded_weight_total",
|
||||
"initial_tool", "initial_extruder", "total_toolchanges", "total_wipe_tower_cost", "total_wipe_tower_filament"})
|
||||
"initial_tool", "initial_extruder", "initial_no_support_extruder", "total_toolchanges", "total_wipe_tower_cost", "total_wipe_tower_filament"})
|
||||
config.set_key_value(key, new ConfigOptionString(std::string("{") + key + "}"));
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -470,6 +470,10 @@ public:
|
||||
std::vector<Polygons> slice_support_volumes(const ModelVolumeType model_volume_type) const;
|
||||
std::vector<Polygons> slice_support_blockers() const { return this->slice_support_volumes(ModelVolumeType::SUPPORT_BLOCKER); }
|
||||
std::vector<Polygons> slice_support_enforcers() const { return this->slice_support_volumes(ModelVolumeType::SUPPORT_ENFORCER); }
|
||||
// Shared slicing path; multiple volumes are united per layer.
|
||||
std::vector<Polygons> slice_modifier_volumes(const std::vector<const ModelVolume*> &volumes) const;
|
||||
// Keep Precise Seam volumes separate so their individual priority is preserved.
|
||||
std::vector<Polygons> slice_single_volume(const ModelVolume* volume) const { return this->slice_modifier_volumes({volume}); }
|
||||
|
||||
// Helpers to project custom facets on slices
|
||||
void project_and_append_custom_facets(bool seam, EnforcerBlockerType type, std::vector<Polygons>& expolys, std::vector<std::pair<Vec3f,Vec3f>>* vertical_points=nullptr) const;
|
||||
@@ -830,6 +834,7 @@ struct PrintStatistics
|
||||
double total_wipe_tower_cost;
|
||||
double total_wipe_tower_filament;
|
||||
unsigned int initial_tool;
|
||||
unsigned int initial_no_support_tool;
|
||||
std::map<size_t, double> filament_stats;
|
||||
|
||||
// Config with the filled in print statistics.
|
||||
@@ -848,6 +853,7 @@ struct PrintStatistics
|
||||
total_wipe_tower_cost = 0.;
|
||||
total_wipe_tower_filament = 0.;
|
||||
initial_tool = 0;
|
||||
initial_no_support_tool = 0;
|
||||
filament_stats.clear();
|
||||
}
|
||||
static const std::string FilamentUsedG;
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Add or remove support modifier ModelVolumes from model_object_dst to match the ModelVolumes of model_object_new
|
||||
// Add or remove support and Precise Seam modifier ModelVolumes from model_object_dst to match the ModelVolumes of model_object_new
|
||||
// in the exact order and with the same IDs.
|
||||
// It is expected, that the model_object_dst already contains the non-support volumes of model_object_new in the correct order.
|
||||
// Other volume types must already match model_object_new in the correct order.
|
||||
// Friend to ModelVolume to allow copying.
|
||||
// static is not accepted by gcc if declared as a friend of ModelObject.
|
||||
/* static */ void model_volume_list_update_supports(ModelObject &model_object_dst, const ModelObject &model_object_new)
|
||||
/* static */ void model_volume_list_update_supports_and_seams(ModelObject &model_object_dst, const ModelObject &model_object_new)
|
||||
{
|
||||
typedef std::pair<const ModelVolume*, bool> ModelVolumeWithStatus;
|
||||
std::vector<ModelVolumeWithStatus> old_volumes;
|
||||
@@ -33,18 +33,22 @@ namespace Slic3r {
|
||||
assert(! it->second); // not consumed yet
|
||||
it->second = true;
|
||||
ModelVolume *model_volume_dst = const_cast<ModelVolume*>(it->first);
|
||||
// For support modifiers, the type may have been switched from blocker to enforcer and vice versa.
|
||||
assert((model_volume_dst->is_support_modifier() && model_volume_src->is_support_modifier()) || model_volume_dst->type() == model_volume_src->type());
|
||||
// Type may switch within support family, within precise_seam family, or between them.
|
||||
assert((model_volume_dst->is_support_modifier() && model_volume_src->is_support_modifier()) ||
|
||||
(model_volume_dst->is_precise_seam() && model_volume_src->is_precise_seam()) ||
|
||||
(model_volume_dst->is_support_modifier() && model_volume_src->is_precise_seam()) ||
|
||||
(model_volume_dst->is_precise_seam() && model_volume_src->is_support_modifier()) ||
|
||||
model_volume_dst->type() == model_volume_src->type());
|
||||
model_object_dst.volumes.emplace_back(model_volume_dst);
|
||||
if (model_volume_dst->is_support_modifier()) {
|
||||
// For support modifiers, the type may have been switched from blocker to enforcer and vice versa.
|
||||
if (model_volume_dst->is_support_modifier() || model_volume_dst->is_precise_seam()) {
|
||||
// Type may have been switched within or between support/precise_seam families.
|
||||
model_volume_dst->set_type(model_volume_src->type());
|
||||
model_volume_dst->set_transformation(model_volume_src->get_transformation());
|
||||
}
|
||||
assert(model_volume_dst->get_matrix().isApprox(model_volume_src->get_matrix()));
|
||||
} else {
|
||||
// The volume was not found in the old list. Create a new copy.
|
||||
assert(model_volume_src->is_support_modifier());
|
||||
assert(model_volume_src->is_support_modifier() || model_volume_src->is_precise_seam());
|
||||
model_object_dst.volumes.emplace_back(new ModelVolume(*model_volume_src));
|
||||
model_object_dst.volumes.back()->set_model_object(&model_object_dst);
|
||||
}
|
||||
@@ -55,17 +59,20 @@ namespace Slic3r {
|
||||
delete mv_with_status.first;
|
||||
}
|
||||
|
||||
static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, const ModelVolumeType type)
|
||||
// Copy configs of ModelVolumes matching type_filter predicate from src to dst.
|
||||
// Mirrors the template pattern of model_volume_list_changed() in Model.cpp.
|
||||
template<typename TypeFilterFn>
|
||||
static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, TypeFilterFn type_filter)
|
||||
{
|
||||
size_t i_src, i_dst;
|
||||
for (i_src = 0, i_dst = 0; i_src < model_object_src.volumes.size() && i_dst < model_object_dst.volumes.size();) {
|
||||
const ModelVolume &mv_src = *model_object_src.volumes[i_src];
|
||||
ModelVolume &mv_dst = *model_object_dst.volumes[i_dst];
|
||||
if (mv_src.type() != type) {
|
||||
if (! type_filter(mv_src.type())) {
|
||||
++ i_src;
|
||||
continue;
|
||||
}
|
||||
if (mv_dst.type() != type) {
|
||||
if (! type_filter(mv_dst.type())) {
|
||||
++ i_dst;
|
||||
continue;
|
||||
}
|
||||
@@ -88,6 +95,20 @@ static inline void model_volume_list_copy_configs(ModelObject &model_object_dst,
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience overload: single volume type.
|
||||
static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, const ModelVolumeType type)
|
||||
{
|
||||
model_volume_list_copy_configs(model_object_dst, model_object_src, [type](const ModelVolumeType t) { return t == type; });
|
||||
}
|
||||
|
||||
// Convenience overload: multiple volume types at once (e.g. all precise seam types).
|
||||
static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, const std::initializer_list<ModelVolumeType> &types)
|
||||
{
|
||||
model_volume_list_copy_configs(model_object_dst, model_object_src, [&types](const ModelVolumeType t) {
|
||||
return std::find(types.begin(), types.end(), t) != types.end();
|
||||
});
|
||||
}
|
||||
|
||||
static inline void layer_height_ranges_copy_configs(t_layer_config_ranges &lr_dst, const t_layer_config_ranges &lr_src)
|
||||
{
|
||||
assert(lr_dst.size() == lr_src.size());
|
||||
@@ -1296,7 +1317,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
extruder_count, extruder_volume_type_count, filament_keys,
|
||||
"filament_self_index", "filament_extruder_variant",
|
||||
&dynamic_slot_indices);
|
||||
else if ((extruder_count > 1) || different_extruder)
|
||||
// Orca: also on a printer with a single extruder variant once a filament defines several
|
||||
// (e.g. Standard and High Flow), so each filament takes its variant for that extruder.
|
||||
else if ((extruder_count > 1) || different_extruder || new_full_config.has_multi_variant_filament())
|
||||
new_full_config.update_values_to_printer_extruders_for_multiple_filaments(m_ori_full_print_config, extruder_count, extruder_volume_type_count, filament_keys,
|
||||
"filament_self_index", "filament_extruder_variant");
|
||||
}
|
||||
@@ -1618,6 +1641,11 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
|
||||
// 3) Synchronize ModelObjects & PrintObjects.
|
||||
const std::initializer_list<ModelVolumeType> solid_or_modifier_types { ModelVolumeType::MODEL_PART, ModelVolumeType::NEGATIVE_VOLUME, ModelVolumeType::PARAMETER_MODIFIER };
|
||||
const std::initializer_list<ModelVolumeType> precise_seam_types {
|
||||
ModelVolumeType::PRECISE_SEAM_CENTER, ModelVolumeType::PRECISE_SEAM_LEFT,
|
||||
ModelVolumeType::PRECISE_SEAM_RIGHT, ModelVolumeType::PRECISE_SEAM_ENFORCED,
|
||||
ModelVolumeType::PRECISE_SEAM_BLOCKED, ModelVolumeType::PRECISE_SEAM_NEUTRAL
|
||||
};
|
||||
for (size_t idx_model_object = 0; idx_model_object < model.objects.size(); ++ idx_model_object) {
|
||||
ModelObject &model_object = *m_model.objects[idx_model_object];
|
||||
ModelObjectStatus &model_object_status = const_cast<ModelObjectStatus&>(model_object_status_db.reuse(model_object));
|
||||
@@ -1635,6 +1663,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
model_fuzzy_skin_data_changed(model_object, model_object_new);
|
||||
bool supports_differ = model_volume_list_changed(model_object, model_object_new, ModelVolumeType::SUPPORT_BLOCKER) ||
|
||||
model_volume_list_changed(model_object, model_object_new, ModelVolumeType::SUPPORT_ENFORCER);
|
||||
bool precise_seam_differ = model_volume_list_changed(model_object, model_object_new, precise_seam_types);
|
||||
bool layer_height_ranges_differ = ! layer_height_ranges_equal(model_object.layer_config_ranges, model_object_new.layer_config_ranges, model_object_new.layer_height_profile.empty());
|
||||
bool model_origin_translation_differ = model_object.origin_translation != model_object_new.origin_translation;
|
||||
bool brim_points_differ = model_brim_points_data_changed(model_object, model_object_new);
|
||||
@@ -1677,13 +1706,20 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
// Invalidate just the supports step.
|
||||
for (const PrintObjectStatus &print_object_status : print_objects_range)
|
||||
update_apply_status(print_object_status.print_object->invalidate_step(posSupportMaterial));
|
||||
if (supports_differ) {
|
||||
// Copy just the support volumes.
|
||||
model_volume_list_update_supports(model_object, model_object_new);
|
||||
}
|
||||
}
|
||||
if (precise_seam_differ) {
|
||||
// First stop background processing before shuffling or deleting the ModelVolumes in the ModelObject's list.
|
||||
this->call_cancel_callback();
|
||||
update_apply_status(false);
|
||||
// Invalidate seam placement (affects G-code export).
|
||||
update_apply_status(this->invalidate_step(psGCodeExport));
|
||||
} else if (model_custom_seam_data_changed(model_object, model_object_new)) {
|
||||
update_apply_status(this->invalidate_step(psGCodeExport));
|
||||
}
|
||||
// Synchronize both families once, after cancellation and all affected-step invalidations.
|
||||
// This also handles type changes between supports and Precise Seam before copying configs below.
|
||||
if (supports_differ || precise_seam_differ)
|
||||
model_volume_list_update_supports_and_seams(model_object, model_object_new);
|
||||
if (brim_points_differ) {
|
||||
model_object.brim_points = model_object_new.brim_points;
|
||||
update_apply_status(this->invalidate_all_steps());
|
||||
@@ -1708,6 +1744,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
//FIXME What to do with m_material_id?
|
||||
model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, ModelVolumeType::MODEL_PART);
|
||||
model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, ModelVolumeType::PARAMETER_MODIFIER);
|
||||
// Synchronize Precise Seam modifier volumes
|
||||
model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, precise_seam_types);
|
||||
layer_height_ranges_copy_configs(model_object.layer_config_ranges /* dst */, model_object_new.layer_config_ranges /* src */);
|
||||
// Copy the ModelObject name, input_file and instances. The instances will be compared against PrintObject instances in the next step.
|
||||
model_object.name = model_object_new.name;
|
||||
|
||||
@@ -62,7 +62,8 @@ public:
|
||||
SlicingReplaceInitEmptyLayers,
|
||||
SlicingNeedSupportOn,
|
||||
SlicingEmptyGcodeLayers,
|
||||
SlicingGcodeOverlap
|
||||
SlicingGcodeOverlap,
|
||||
SlicingPreciseSeamWarning
|
||||
};
|
||||
|
||||
typedef size_t TimeStamp;
|
||||
|
||||
@@ -626,7 +626,9 @@ static const t_config_enum_values s_keys_map_NozzleVolumeType = {
|
||||
{ "Standard", nvtStandard },
|
||||
{ "High Flow", nvtHighFlow },
|
||||
{ "TPU High Flow", nvtTPUHighFlow },
|
||||
{ "Hybrid", nvtHybrid }
|
||||
{ "Hybrid", nvtHybrid },
|
||||
{ "E3D High Flow", nvtE3DHighFlow },
|
||||
{ "Extra High Flow", nvtExtraHighFlow }
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NozzleVolumeType)
|
||||
|
||||
@@ -676,9 +678,34 @@ int get_config_index_base(NozzleVolumeType volume_type, ExtruderType extruder_ty
|
||||
for (int index = 0; index < int(variant_list.size()); ++index) {
|
||||
if (extruder_variant == variant_list[index] && variant_ids_1based[index] == variant_id_1based) { return index; }
|
||||
}
|
||||
// Without this variant, use the id's own first variant (usually Standard), not variant index 0,
|
||||
// which belongs to the first filament or extruder.
|
||||
for (int index = 0; index < int(variant_list.size()); ++index) {
|
||||
if (variant_ids_1based[index] == variant_id_1based) { return index; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::set<NozzleVolumeType> get_extruder_supported_nozzle_volume_types(const DynamicPrintConfig &printer_config, int extruder_id)
|
||||
{
|
||||
std::set<NozzleVolumeType> supported_types;
|
||||
|
||||
auto *variant_list = printer_config.option<ConfigOptionStrings>("extruder_variant_list");
|
||||
auto *extruder_types = printer_config.option<ConfigOptionEnumsGeneric>("extruder_type");
|
||||
if (!variant_list || !extruder_types || extruder_id < 0 ||
|
||||
extruder_id >= (int) variant_list->values.size() || extruder_id >= (int) extruder_types->values.size())
|
||||
return supported_types;
|
||||
|
||||
const ExtruderType extruder_type = ExtruderType(extruder_types->values[extruder_id]);
|
||||
for (NozzleVolumeType volume_type : get_valid_nozzle_volume_type()) {
|
||||
// An unsupported extruder type yields an empty name, which would match any list.
|
||||
const std::string variant = get_extruder_variant_string(extruder_type, volume_type);
|
||||
if (!variant.empty() && variant_list->values[extruder_id].find(variant) != std::string::npos)
|
||||
supported_types.insert(volume_type);
|
||||
}
|
||||
return supported_types;
|
||||
}
|
||||
|
||||
std::string get_nozzle_volume_type_string(NozzleVolumeType nozzle_volume_type)
|
||||
{
|
||||
if (nozzle_volume_type > nvtMaxNozzleVolumeType) {
|
||||
@@ -5923,15 +5950,20 @@ void PrintConfigDef::init_fff_params()
|
||||
def->label = "Nozzle Volume Type";
|
||||
def->tooltip = "Nozzle volume type for extruders.";
|
||||
def->enum_keys_map = &ConfigOptionEnum<NozzleVolumeType>::get_enum_values();
|
||||
// Order must match the NozzleVolumeType enum values (Standard=0, High Flow=1, Hybrid=2, TPU High Flow=3).
|
||||
// Listed in display order. A position is not the enum value (E3D High Flow is 5, after the reserved 4),
|
||||
// so map a position to its NozzleVolumeType through enum_keys_map.
|
||||
def->enum_values.push_back(L("Standard"));
|
||||
def->enum_values.push_back(L("High Flow"));
|
||||
def->enum_values.push_back(L("Hybrid"));
|
||||
def->enum_values.push_back(L("TPU High Flow"));
|
||||
def->enum_values.push_back(L("E3D High Flow"));
|
||||
def->enum_values.push_back(L("Extra High Flow"));
|
||||
def->enum_labels.push_back(L("Standard"));
|
||||
def->enum_labels.push_back(L("High Flow"));
|
||||
def->enum_labels.push_back(L("Hybrid"));
|
||||
def->enum_labels.push_back(L("TPU High Flow"));
|
||||
def->enum_labels.push_back(L("E3D High Flow"));
|
||||
def->enum_labels.push_back(L("Extra High Flow"));
|
||||
def->mode = comSimple;
|
||||
def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard });
|
||||
|
||||
@@ -5944,10 +5976,14 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_values.push_back(L("High Flow"));
|
||||
def->enum_values.push_back(L("Hybrid"));
|
||||
def->enum_values.push_back(L("TPU High Flow"));
|
||||
def->enum_values.push_back(L("E3D High Flow"));
|
||||
def->enum_values.push_back(L("Extra High Flow"));
|
||||
def->enum_labels.push_back(L("Standard"));
|
||||
def->enum_labels.push_back(L("High Flow"));
|
||||
def->enum_labels.push_back(L("Hybrid"));
|
||||
def->enum_labels.push_back(L("TPU High Flow"));
|
||||
def->enum_labels.push_back(L("E3D High Flow"));
|
||||
def->enum_labels.push_back(L("Extra High Flow"));
|
||||
def->mode = comDevelop;
|
||||
def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard });
|
||||
|
||||
@@ -5986,7 +6022,7 @@ void PrintConfigDef::init_fff_params()
|
||||
// Per-nozzle volume type. Forward-compat-only registration with no slicing consumer — nothing in
|
||||
// src/ reads it; the engine resolves per-nozzle volume types from `extruder_nozzle_stats` tokens
|
||||
// instead. Kept registered so a project/config carrying it loads without an unknown-option
|
||||
// substitution warning. Registers Standard/High Flow/TPU High Flow only (no Hybrid).
|
||||
// substitution warning. Registers the physical types only (no Hybrid).
|
||||
// Internal use only, no translation.
|
||||
def = this->add("extruder_nozzle_volume_type", coEnums);
|
||||
def->label = "Extruder nozzle volume type";
|
||||
@@ -5995,9 +6031,13 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_values.push_back("Standard");
|
||||
def->enum_values.push_back("High Flow");
|
||||
def->enum_values.push_back("TPU High Flow");
|
||||
def->enum_values.push_back("E3D High Flow");
|
||||
def->enum_values.push_back("Extra High Flow");
|
||||
def->enum_labels.push_back("Standard");
|
||||
def->enum_labels.push_back("High Flow");
|
||||
def->enum_labels.push_back("TPU High Flow");
|
||||
def->enum_labels.push_back("E3D High Flow");
|
||||
def->enum_labels.push_back("Extra High Flow");
|
||||
def->mode = comDevelop;
|
||||
def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard });
|
||||
|
||||
@@ -9363,6 +9403,10 @@ void PrintConfigDef::handle_legacy_composite(DynamicPrintConfig &config)
|
||||
}
|
||||
config.set_key_value("wiping_volumes_use_custom_matrix", new ConfigOptionBool(custom));
|
||||
}
|
||||
|
||||
// Orca: a config saved before a key joined filament_options_with_variant stores it once per filament
|
||||
// rather than once per filament variant, and one exported by an older CLI may store a single value.
|
||||
normalize_filament_values_to_variants(config);
|
||||
}
|
||||
|
||||
const PrintConfigDef print_config_def;
|
||||
@@ -9465,6 +9509,13 @@ std::set<std::string> filament_options_with_variant = {
|
||||
"filament_ironing_spacing",
|
||||
"filament_ironing_inset",
|
||||
"filament_ironing_speed",
|
||||
// Orca: pressure advance
|
||||
"enable_pressure_advance",
|
||||
"pressure_advance",
|
||||
"adaptive_pressure_advance",
|
||||
"adaptive_pressure_advance_model",
|
||||
"adaptive_pressure_advance_overhangs",
|
||||
"adaptive_pressure_advance_bridges",
|
||||
"activate_air_filtration",
|
||||
"activate_air_filtration_during_print",
|
||||
"activate_air_filtration_on_completion",
|
||||
@@ -10070,6 +10121,13 @@ bool DynamicPrintConfig::is_using_different_extruders()
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool DynamicPrintConfig::has_multi_variant_filament() const
|
||||
{
|
||||
auto variants = dynamic_cast<const ConfigOptionStrings*>(this->option("filament_extruder_variant"));
|
||||
auto diameters = dynamic_cast<const ConfigOptionFloats*>(this->option("filament_diameter"));
|
||||
return variants && diameters && variants->size() > diameters->size();
|
||||
}
|
||||
|
||||
bool DynamicPrintConfig::support_different_extruders(int& extruder_count) const
|
||||
{
|
||||
std::set<std::string> variant_set;
|
||||
@@ -10665,6 +10723,25 @@ void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVect
|
||||
target.set_to_index(&source, indices, stride);
|
||||
}
|
||||
|
||||
void normalize_filament_values_to_variants(DynamicPrintConfig &config)
|
||||
{
|
||||
const auto *self_index = config.option<ConfigOptionInts>("filament_self_index");
|
||||
if (self_index == nullptr || self_index->empty())
|
||||
return;
|
||||
const int filament_count = *std::max_element(self_index->values.begin(), self_index->values.end());
|
||||
if (filament_count <= 0 || size_t(filament_count) >= self_index->size())
|
||||
return;
|
||||
for (const std::string &key : filament_options_with_variant) {
|
||||
auto *opt = dynamic_cast<ConfigOptionVectorBase *>(config.option(key));
|
||||
if (opt == nullptr || (opt->size() != size_t(filament_count) && opt->size() != 1))
|
||||
continue;
|
||||
std::unique_ptr<ConfigOption> per_filament(opt->clone());
|
||||
// set_at() takes the first value for a filament past the end of a single-value vector
|
||||
for (size_t variant = 0; variant < self_index->size(); ++variant)
|
||||
opt->set_at(per_filament.get(), variant, self_index->values[variant] - 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//used for object/region config
|
||||
//use the smallest of multiple to single
|
||||
@@ -11070,6 +11147,11 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
|
||||
return;
|
||||
}
|
||||
std::vector<int> filament_maps = opt_filament_map->values;
|
||||
auto opt_ids = id_name.empty()? nullptr: dynamic_cast<const ConfigOptionInts*>(this->option(id_name));
|
||||
// Orca: a map shorter than the filament count must not drop the filaments past its end;
|
||||
// they take the first extruder.
|
||||
if (opt_ids && !opt_ids->values.empty())
|
||||
filament_maps.resize(std::max<size_t>(filament_maps.size(), *std::max_element(opt_ids->values.begin(), opt_ids->values.end())), 1);
|
||||
size_t filament_count = filament_maps.size();
|
||||
//apply process settings
|
||||
auto opt_extruder_type = dynamic_cast<const ConfigOptionEnumsGeneric*>(printer_config.option("extruder_type"));
|
||||
@@ -11088,7 +11170,6 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
|
||||
// indexed out of bounds.
|
||||
if (opt_filament_volume_maps && opt_filament_volume_maps->values.size() == filament_count)
|
||||
filament_volume_maps = opt_filament_volume_maps->values;
|
||||
auto opt_ids = id_name.empty()? nullptr: dynamic_cast<const ConfigOptionInts*>(this->option(id_name));
|
||||
std::vector<int> variant_index;
|
||||
|
||||
variant_index.resize(filament_count, -1);
|
||||
@@ -11105,9 +11186,10 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
|
||||
//variant index
|
||||
variant_index[f_index] = get_index_for_extruder(f_index+1, id_name, extruder_type, nozzle_volume_type, variant_name);
|
||||
if (variant_index[f_index] < 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", Line %1%: could not found extruder_type %2%, nozzle_volume_type %3%, filament_index %4%, extruder index %5%")
|
||||
// Orca: a filament need not define every extruder variant (a Direct Drive filament on a
|
||||
// Bowden printer), so this is not an invalid state: the filament's first variant is used.
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: could not found extruder_type %2%, nozzle_volume_type %3%, filament_index %4%, extruder index %5%")
|
||||
%__LINE__ %s_keys_names_ExtruderType[extruder_type] % s_keys_names_NozzleVolumeType[nozzle_volume_type] % (f_index+1) %filament_maps[f_index];
|
||||
assert(false);
|
||||
//for some updates happens in a invalid state(caused by popup window)
|
||||
//we need to avoid crash
|
||||
variant_index[f_index] = 0;
|
||||
@@ -12513,6 +12595,7 @@ OtherSlicingStatesConfigDef::OtherSlicingStatesConfigDef()
|
||||
|
||||
new_def("initial_no_support_extruder", coInt, "Initial no support extruder", "Zero-based index of the first extruder used for printing without support. Same as initial_no_support_tool.");
|
||||
new_def("in_head_wrap_detect_zone", coBool, "In head wrap detect zone", "Indicates if the first layer overlaps with the head wrap zone.");
|
||||
new_def("curr_bed_type", coString, "Current bed type", "Name of the currently selected bed plate type (e.g. 'Textured PEI Plate', 'Smooth High Temp Plate').");
|
||||
}
|
||||
|
||||
PrintStatisticsConfigDef::PrintStatisticsConfigDef()
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class DynamicPrintConfig;
|
||||
|
||||
enum GCodeFlavor : unsigned char {
|
||||
gcfMarlinLegacy,
|
||||
gcfKlipper,
|
||||
@@ -519,8 +521,11 @@ enum NozzleVolumeType {
|
||||
// with more than one sub-nozzle (extruder_max_nozzle_count > 1); matched as Standard for
|
||||
// preset lookup and never emitted in profile variant strings
|
||||
nvtTPUHighFlow, // physical variant, used on H2D/H2DP 0.4 nozzles only
|
||||
// 4 is reserved: E3D High Flow is 5 in BambuStudio's slice_info and device numbering.
|
||||
nvtE3DHighFlow = 5, // physical variant, E3D high-flow hotend on 0.4/0.6 nozzles
|
||||
nvtExtraHighFlow = 6, // Orca: physical variant with no BambuStudio or device counterpart; only profiles name it
|
||||
// Integer values are serialized as raw ints in 3mf plate metadata and device MQTT, so they MUST stay stable.
|
||||
nvtMaxNozzleVolumeType = nvtTPUHighFlow
|
||||
nvtMaxNozzleVolumeType = nvtExtraHighFlow
|
||||
};
|
||||
|
||||
enum FilamentMapMode {
|
||||
@@ -558,11 +563,19 @@ static std::set<NozzleVolumeType> get_valid_nozzle_volume_type() {
|
||||
// Hybrid is not a physical nozzle variant: presets never define it, so it must not
|
||||
// produce a variant string.
|
||||
if (t == nvtHybrid) continue;
|
||||
// Skip the reserved gap between nvtTPUHighFlow (3) and nvtE3DHighFlow (5).
|
||||
if (i > nvtTPUHighFlow && i < nvtE3DHighFlow) continue;
|
||||
type.insert(t);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
// The nozzle volume types the given extruder physically provides, as declared by the printer
|
||||
// profile's extruder_variant_list. An empty set means the profile could not be read and must be
|
||||
// treated as "unknown", not as "none". nvtHybrid is never reported: it describes an extruder
|
||||
// holding a mix of nozzles, not a nozzle the profile can offer.
|
||||
extern std::set<NozzleVolumeType> get_extruder_supported_nozzle_volume_types(const DynamicPrintConfig &printer_config, int extruder_id);
|
||||
|
||||
std::string get_nozzle_volume_type_string(NozzleVolumeType nozzle_volume_type);
|
||||
|
||||
static std::string bed_type_to_gcode_string(const BedType type)
|
||||
@@ -699,8 +712,6 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder)
|
||||
|
||||
#undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
|
||||
|
||||
class DynamicPrintConfig;
|
||||
|
||||
// Defines each and every configuration option of Slic3r, including the properties of the GUI dialogs.
|
||||
// Does not store the actual values, but defines default values.
|
||||
class PrintConfigDef : public ConfigDef
|
||||
@@ -821,6 +832,10 @@ public:
|
||||
//BBS
|
||||
bool is_using_different_extruders();
|
||||
bool support_different_extruders(int& extruder_count) const;
|
||||
// Whether any filament defines more than one variant (filament_extruder_variant longer than
|
||||
// filament_diameter). Its variants then have to be resolved even on a printer with a single
|
||||
// extruder variant, which picks the filament's variant of the same variant string.
|
||||
bool has_multi_variant_filament() const;
|
||||
// Counts the config slots of a printer: one per (extruder x nozzle volume type) as described by
|
||||
// extruder_nozzle_stats, or simply one per extruder when the stats are absent/mismatched.
|
||||
// Fills nozzle_volume_types with each extruder's volume types in ascending enum order.
|
||||
@@ -869,6 +884,11 @@ extern std::set<std::string> empty_options;
|
||||
void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVectorBase &source,
|
||||
const std::vector<int> &variant_index, int stride = 1);
|
||||
|
||||
// Orca: lays every filament_options_with_variant option out one value per filament variant, as
|
||||
// filament_self_index maps the variants to filaments. An option holding one value per filament, or a
|
||||
// single value, gives every variant of a filament that filament's value; other lengths are left alone.
|
||||
void normalize_filament_values_to_variants(DynamicPrintConfig &config);
|
||||
|
||||
extern std::set<std::string> filament_dev_options;
|
||||
|
||||
extern void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPrintConfig& dest_config, std::vector<int> variant_index, std::set<std::string>& key_set1, int stride = 1);
|
||||
|
||||
@@ -1551,12 +1551,19 @@ ExPolygons PrintObject::_shrink_contour_holes(double contour_delta, double hole_
|
||||
|
||||
std::vector<Polygons> PrintObject::slice_support_volumes(const ModelVolumeType model_volume_type) const
|
||||
{
|
||||
auto it_volume = this->model_object()->volumes.begin();
|
||||
auto it_volume_end = this->model_object()->volumes.end();
|
||||
for (; it_volume != it_volume_end && (*it_volume)->type() != model_volume_type; ++ it_volume) ;
|
||||
// Supports merge every matching volume; Precise Seam calls the shared slicer one volume at a time.
|
||||
std::vector<const ModelVolume*> volumes;
|
||||
for (const ModelVolume *volume : this->model_object()->volumes)
|
||||
if (volume->type() == model_volume_type)
|
||||
volumes.push_back(volume);
|
||||
return this->slice_modifier_volumes(volumes);
|
||||
}
|
||||
|
||||
std::vector<Polygons> PrintObject::slice_modifier_volumes(const std::vector<const ModelVolume*> &volumes) const
|
||||
{
|
||||
std::vector<Polygons> slices;
|
||||
if (it_volume != it_volume_end) {
|
||||
// Found at least a single support volume of model_volume_type.
|
||||
if (!volumes.empty()) {
|
||||
// Share layer heights, transforms and cancellation handling across the selected volumes.
|
||||
std::vector<float> zs = zs_from_layers(this->layers());
|
||||
std::vector<char> merge_layers;
|
||||
bool merge = false;
|
||||
@@ -1564,27 +1571,26 @@ std::vector<Polygons> PrintObject::slice_support_volumes(const ModelVolumeType m
|
||||
auto throw_on_cancel_callback = std::function<void()>([print](){ print->throw_if_canceled(); });
|
||||
MeshSlicingParamsEx params;
|
||||
params.trafo = this->trafo_centered();
|
||||
for (; it_volume != it_volume_end; ++ it_volume)
|
||||
if ((*it_volume)->type() == model_volume_type) {
|
||||
std::vector<ExPolygons> slices2 = slice_volume(*(*it_volume), zs, params, throw_on_cancel_callback);
|
||||
if (slices.empty()) {
|
||||
slices.reserve(slices2.size());
|
||||
for (ExPolygons &src : slices2)
|
||||
slices.emplace_back(to_polygons(std::move(src)));
|
||||
} else if (!slices2.empty()) {
|
||||
if (merge_layers.empty())
|
||||
merge_layers.assign(zs.size(), false);
|
||||
for (size_t i = 0; i < zs.size(); ++ i) {
|
||||
if (slices[i].empty())
|
||||
slices[i] = to_polygons(std::move(slices2[i]));
|
||||
else if (! slices2[i].empty()) {
|
||||
append(slices[i], to_polygons(std::move(slices2[i])));
|
||||
merge_layers[i] = true;
|
||||
merge = true;
|
||||
}
|
||||
for (const ModelVolume *volume : volumes) {
|
||||
std::vector<ExPolygons> slices2 = slice_volume(*volume, zs, params, throw_on_cancel_callback);
|
||||
if (slices.empty()) {
|
||||
slices.reserve(slices2.size());
|
||||
for (ExPolygons &src : slices2)
|
||||
slices.emplace_back(to_polygons(std::move(src)));
|
||||
} else if (!slices2.empty()) {
|
||||
if (merge_layers.empty())
|
||||
merge_layers.assign(zs.size(), false);
|
||||
for (size_t i = 0; i < zs.size(); ++ i) {
|
||||
if (slices[i].empty())
|
||||
slices[i] = to_polygons(std::move(slices2[i]));
|
||||
else if (! slices2[i].empty()) {
|
||||
append(slices[i], to_polygons(std::move(slices2[i])));
|
||||
merge_layers[i] = true;
|
||||
merge = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (merge) {
|
||||
std::vector<Polygons*> to_merge;
|
||||
to_merge.reserve(zs.size());
|
||||
|
||||
@@ -1778,6 +1778,13 @@ TriangleSelector::TriangleSplittingData TriangleSelector::serialize() const {
|
||||
return out.data;
|
||||
}
|
||||
|
||||
// A split code keeps the split side (one split) or the kept side (two splits) in its upper two
|
||||
// bits, where 3 is not a side. The value is ignored for a three-side split.
|
||||
static bool split_code_valid(int code)
|
||||
{
|
||||
return (code & 0b11) == 3 || (code >> 2) != 3;
|
||||
}
|
||||
|
||||
void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
||||
bool needs_reset,
|
||||
EnforcerBlockerType max_ebt,
|
||||
@@ -1812,11 +1819,12 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
||||
|
||||
for (auto [triangle_id, ibit] : data.triangles_to_split) {
|
||||
assert(triangle_id < int(m_triangles.size()));
|
||||
assert(ibit < int(data.bitstream.size()));
|
||||
auto next_nibble = [&data, &ibit = ibit]() {
|
||||
// Set when the bitstream runs out or holds an impossible split before this triangle's tree is complete.
|
||||
bool corrupt = false;
|
||||
auto next_nibble = [&data, &ibit = ibit, &corrupt]() {
|
||||
int n = 0;
|
||||
for (int i = 0; i < 4; ++ i)
|
||||
n |= data.bitstream[ibit ++] << i;
|
||||
if (! data.read_nibble(ibit, n))
|
||||
corrupt = true;
|
||||
return n;
|
||||
};
|
||||
// Decode a leaf state stored behind the "11" prefix: one nibble of (state-3) for states
|
||||
@@ -1835,6 +1843,10 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
||||
bool is_split = num_of_children != 0;
|
||||
// Only valid if not is_split.
|
||||
auto state = is_split ? EnforcerBlockerType::NONE : ((code & 0b1100) == 0b1100 ? decode_leaf_state() : EnforcerBlockerType(code >> 2));
|
||||
if (is_split && ! split_code_valid(code))
|
||||
corrupt = true;
|
||||
if (corrupt)
|
||||
break;
|
||||
|
||||
// BBS
|
||||
if (state == to_delete_filament)
|
||||
@@ -1849,7 +1861,7 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
||||
}
|
||||
|
||||
// Only valid if is_split.
|
||||
int special_side = code >> 2;
|
||||
int special_side = num_of_split_sides == 3 ? 0 : code >> 2;
|
||||
|
||||
// Take care of the first iteration separately, so handling of the others is simpler.
|
||||
if (parents.empty()) {
|
||||
@@ -1904,47 +1916,55 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
||||
if (parents.empty())
|
||||
break;
|
||||
}
|
||||
|
||||
if (corrupt) {
|
||||
// Every split above allocated all of its children, so the partial tree unwinds cleanly.
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": malformed paint data, dropping paint of triangle " << triangle_id;
|
||||
undivide_triangle(triangle_id);
|
||||
m_triangles[triangle_id].set_state(EnforcerBlockerType::NONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TriangleSelector::TriangleSplittingData::update_used_states(const size_t bitstream_start_idx) {
|
||||
assert(bitstream_start_idx < this->bitstream.size());
|
||||
assert(!this->bitstream.empty() && this->bitstream.size() != bitstream_start_idx);
|
||||
assert((this->bitstream.size() - bitstream_start_idx) % 4 == 0);
|
||||
bool TriangleSelector::TriangleSplittingData::update_used_states(const size_t bitstream_start_idx) {
|
||||
int ibit = static_cast<int>(bitstream_start_idx);
|
||||
uint64_t states = 0;
|
||||
do {
|
||||
// Walk one triangle's tree depth-first, counting the nodes still to be read; a split node adds its children.
|
||||
for (int pending_nodes = 1; pending_nodes > 0; --pending_nodes) {
|
||||
int code;
|
||||
if (!this->read_nibble(ibit, code))
|
||||
return false;
|
||||
|
||||
if (this->bitstream.empty() || this->bitstream.size() == bitstream_start_idx)
|
||||
return;
|
||||
if (const int num_of_split_sides = code & 0b11; num_of_split_sides != 0) {
|
||||
if (!split_code_valid(code))
|
||||
return false;
|
||||
pending_nodes += num_of_split_sides + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t nibble_idx = bitstream_start_idx;
|
||||
|
||||
auto read_next_nibble = [&data_bitstream = std::as_const(this->bitstream), &nibble_idx]() -> uint8_t {
|
||||
assert(nibble_idx + 3 < data_bitstream.size());
|
||||
uint8_t code = 0;
|
||||
for (size_t bit_idx = 0; bit_idx < 4; ++bit_idx)
|
||||
code |= data_bitstream[nibble_idx++] << bit_idx;
|
||||
return code;
|
||||
};
|
||||
|
||||
while (nibble_idx < this->bitstream.size()) {
|
||||
const uint8_t code = read_next_nibble();
|
||||
|
||||
if (const bool is_split = (code & 0b11) != 0; is_split)
|
||||
continue;
|
||||
|
||||
uint8_t facet_state;
|
||||
if ((code & 0b1100) == 0b1100) {
|
||||
// Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18).
|
||||
const uint8_t nibble = read_next_nibble();
|
||||
facet_state = nibble == 0b1111 ? uint8_t(read_next_nibble() + 18) : uint8_t(nibble + 3);
|
||||
} else {
|
||||
facet_state = code >> 2;
|
||||
int facet_state = code >> 2;
|
||||
if (facet_state == 0b11) {
|
||||
// Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18).
|
||||
int nibble;
|
||||
if (!this->read_nibble(ibit, nibble))
|
||||
return false;
|
||||
facet_state = nibble + 3;
|
||||
if (nibble == 0b1111) {
|
||||
if (!this->read_nibble(ibit, nibble))
|
||||
return false;
|
||||
facet_state = nibble + 18;
|
||||
}
|
||||
}
|
||||
states |= uint64_t(1) << facet_state;
|
||||
}
|
||||
assert(facet_state < this->used_states.size());
|
||||
if (facet_state >= this->used_states.size())
|
||||
continue;
|
||||
} while (static_cast<size_t>(ibit) < this->bitstream.size());
|
||||
|
||||
this->used_states[facet_state] = true;
|
||||
}
|
||||
// The leaf encoding tops out at state 33, so every state fits the 64-bit mask.
|
||||
for (size_t state_idx = 0; state_idx < std::min<size_t>(this->used_states.size(), 64); ++state_idx)
|
||||
if (states & (uint64_t(1) << state_idx))
|
||||
this->used_states[state_idx] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Lightweight variant of deserialization, which only tests whether a face of test_state exists.
|
||||
@@ -1956,11 +1976,12 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor
|
||||
|
||||
for (const TriangleBitStreamMapping &triangle_id_and_ibit : data.triangles_to_split) {
|
||||
int ibit = triangle_id_and_ibit.bitstream_start_idx;
|
||||
assert(ibit < int(data.bitstream.size()));
|
||||
auto next_nibble = [&data, &ibit = ibit]() {
|
||||
// Stop reading a triangle whose stream is truncated.
|
||||
bool truncated = false;
|
||||
auto next_nibble = [&data, &ibit = ibit, &truncated]() {
|
||||
int n = 0;
|
||||
for (int i = 0; i < 4; ++ i)
|
||||
n |= data.bitstream[ibit ++] << i;
|
||||
if (! data.read_nibble(ibit, n))
|
||||
truncated = true;
|
||||
return n;
|
||||
};
|
||||
// < 0 -> negative of a number of children
|
||||
@@ -1978,6 +1999,8 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor
|
||||
};
|
||||
|
||||
int state = num_children_or_state();
|
||||
if (truncated)
|
||||
continue;
|
||||
if (state < 0) {
|
||||
// Root is split.
|
||||
parents_children.clear();
|
||||
@@ -1985,6 +2008,8 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor
|
||||
do {
|
||||
if (-- parents_children.back() >= 0) {
|
||||
int state = num_children_or_state();
|
||||
if (truncated)
|
||||
break;
|
||||
if (state < 0)
|
||||
// Child is split.
|
||||
parents_children.emplace_back(- state);
|
||||
|
||||
@@ -297,8 +297,20 @@ public:
|
||||
std::fill(used_states.begin(), used_states.end(), false);
|
||||
}
|
||||
|
||||
// Update used states based on the bitstream. It just iterated over the bitstream from the bitstream_start_idx till the end.
|
||||
void update_used_states(size_t bitstream_start_idx);
|
||||
// Update used states from the triangle trees stored between bitstream_start_idx and the end of the bitstream.
|
||||
// Returns false and leaves used states untouched if a tree is truncated or malformed.
|
||||
bool update_used_states(size_t bitstream_start_idx);
|
||||
|
||||
// Read the 4-bit code at bit index ibit (LSB first) and advance ibit past it.
|
||||
// Returns false without advancing when fewer than 4 bits remain.
|
||||
bool read_nibble(int &ibit, int &nibble) const {
|
||||
if (ibit < 0 || static_cast<size_t>(ibit) + 4 > bitstream.size())
|
||||
return false;
|
||||
nibble = 0;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
nibble |= static_cast<int>(bitstream[ibit++]) << i;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
friend class cereal::access;
|
||||
|
||||
+25
-1
@@ -256,10 +256,19 @@ 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.
|
||||
// True if rel_path is relative, has no ".." component or embedded NUL 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);
|
||||
// True if a symlink stored at link_rel_path (relative to root) with this target stays inside root: the target
|
||||
// must be relative, and joined to the link's directory it must pass is_path_within_root.
|
||||
extern bool is_symlink_target_within_root(const std::string &link_rel_path, const std::string &target, const boost::filesystem::path &root);
|
||||
// True if path names an entry strictly inside root: it must be spelled with root as its prefix,
|
||||
// and must still resolve inside root once symlinks are followed.
|
||||
extern bool is_absolute_path_within_root(const boost::filesystem::path &path, const boost::filesystem::path &root);
|
||||
// True if a file with this name is of a type that the desktop opens as plain content, so it cannot run code.
|
||||
// Anything unknown is not safe.
|
||||
extern bool is_safe_to_open_file_name(const std::string &file_name);
|
||||
|
||||
// Orca: custom protocal support utils
|
||||
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
||||
@@ -285,6 +294,21 @@ inline std::string sanitize_filename(const std::string &filename){
|
||||
const std::regex special_chars("[/\\\\:*?\"<>|]");
|
||||
return std::regex_replace(filename, special_chars, "_");
|
||||
}
|
||||
// Reduce an untrusted, possibly path-qualified name to a single sanitized file name.
|
||||
// Returns an empty string when nothing usable remains.
|
||||
inline std::string sanitize_file_basename(const std::string &name){
|
||||
const size_t sep = name.find_last_of("/\\");
|
||||
const std::string base = sanitize_filename(sep == std::string::npos ? name : name.substr(sep + 1));
|
||||
// Names made only of dots and spaces refer to the folder or its parent, or are stripped to nothing on Windows.
|
||||
return base.find_first_not_of(". ") == std::string::npos ? std::string() : base;
|
||||
}
|
||||
// Marker file a download of this process writes to before it is renamed to filename.
|
||||
boost::filesystem::path download_marker_path(const boost::filesystem::path &dest_folder, const std::string &filename);
|
||||
// Finds a sanitized variant of filename, "name(N).ext" if needed, that neither an entry of dest_folder
|
||||
// nor the download marker of another download uses. The marker at ignored_marker does not count.
|
||||
// Returns true and the name in result, or false and the last name tried.
|
||||
bool find_unused_filename(const boost::filesystem::path &dest_folder, const std::string &filename,
|
||||
const boost::filesystem::path &ignored_marker, std::string &result);
|
||||
// File path / name / extension splitting utilities, working with UTF-8,
|
||||
// to be published to Perl.
|
||||
namespace PerlUtils {
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include "miniz_extension.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||
#include "boost/nowide/cstdio.hpp"
|
||||
#endif
|
||||
@@ -115,6 +118,66 @@ std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_
|
||||
return decode_zip_unicode_path_extra_field(extra.substr(0, extra_size > 0 ? extra_size - 1 : 0), stat.m_filename);
|
||||
}
|
||||
|
||||
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir)
|
||||
{
|
||||
mz_zip_archive archive;
|
||||
mz_zip_zero_struct(&archive);
|
||||
|
||||
if (!open_zip_reader(&archive, zip_path_utf8)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Unable to open zip reader for " << zip_path_utf8;
|
||||
return false;
|
||||
}
|
||||
|
||||
const mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
|
||||
mz_zip_archive_file_stat stat;
|
||||
|
||||
// Validate every entry first so an archive with a single escaping entry leaves no partial output behind.
|
||||
const boost::filesystem::path root(dest_dir);
|
||||
for (mz_uint i = 0; i < num_entries; ++i) {
|
||||
if (mz_zip_reader_file_stat(&archive, i, &stat) && !is_path_within_root(stat.m_filename, root)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Unzip: rejecting " << zip_path_utf8 << ", entry " << stat.m_filename << " resolves outside " << dest_dir;
|
||||
close_zip_reader(&archive);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (mz_uint i = 0; i < num_entries; ++i) {
|
||||
if (!mz_zip_reader_file_stat(&archive, i, &stat)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Unzip: read file stat failed";
|
||||
continue;
|
||||
}
|
||||
const std::string dest_file = dest_dir + "/" + stat.m_filename;
|
||||
try {
|
||||
if (stat.m_is_directory) {
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
if (!boost::filesystem::exists(dest_path))
|
||||
boost::filesystem::create_directories(dest_path);
|
||||
continue;
|
||||
}
|
||||
if (stat.m_uncomp_size == 0) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Unzip: invalid size for file " << stat.m_filename;
|
||||
continue;
|
||||
}
|
||||
// Replace a symlink at the destination rather than writing through it.
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path)))
|
||||
boost::filesystem::remove(dest_path);
|
||||
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
|
||||
close_zip_reader(&archive);
|
||||
return false;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Unzip: successfully extract file " << stat.m_file_index << " to " << dest_file;
|
||||
} catch (const std::exception &e) {
|
||||
close_zip_reader(&archive);
|
||||
BOOST_LOG_TRIVIAL(error) << "Unzip: archive read exception: " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
close_zip_reader(&archive);
|
||||
return true;
|
||||
}
|
||||
|
||||
MZ_Archive::MZ_Archive()
|
||||
{
|
||||
mz_zip_zero_struct(&arch);
|
||||
|
||||
@@ -11,6 +11,8 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname_utf8);
|
||||
bool close_zip_reader(mz_zip_archive *zip);
|
||||
bool close_zip_writer(mz_zip_archive *zip);
|
||||
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
|
||||
// Extracts every entry of the archive under dest_dir. Nothing is written if any entry would resolve outside dest_dir.
|
||||
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir);
|
||||
|
||||
class MZ_Archive {
|
||||
public:
|
||||
|
||||
+64
-1
@@ -70,6 +70,7 @@
|
||||
#include <boost/shared_ptr.hpp>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
@@ -1093,6 +1094,9 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
||||
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;
|
||||
// The filesystem calls stop at a NUL, so they would act on a shorter path than the one checked here.
|
||||
if (rel_path.find('\0') != std::string::npos)
|
||||
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]))
|
||||
@@ -1103,7 +1107,10 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
||||
}
|
||||
// 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();
|
||||
std::string root_str = boost::filesystem::weakly_canonical(root).string();
|
||||
// A trailing separator on root would otherwise fail the prefix match below for every path.
|
||||
while (!root_str.empty() && (root_str.back() == '/' || root_str.back() == boost::filesystem::path::preferred_separator))
|
||||
root_str.pop_back();
|
||||
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);
|
||||
@@ -1112,6 +1119,37 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
||||
}
|
||||
}
|
||||
|
||||
bool is_symlink_target_within_root(const std::string &link_rel_path, const std::string &target, const boost::filesystem::path &root)
|
||||
{
|
||||
if (target.empty() || target.front() == '/' || target.front() == '\\' || (target.size() > 1 && target[1] == ':'))
|
||||
return false;
|
||||
// A relative target without ".." only descends from the link's directory, so no chain of such links can leave root.
|
||||
const size_t sep = link_rel_path.find_last_of("/\\");
|
||||
return is_path_within_root((sep == std::string::npos ? std::string() : link_rel_path.substr(0, sep + 1)) + target, root);
|
||||
}
|
||||
|
||||
bool is_absolute_path_within_root(const boost::filesystem::path &path, const boost::filesystem::path &root)
|
||||
{
|
||||
const boost::filesystem::path rel = path.lexically_relative(root);
|
||||
return !rel.empty() && rel != "." && is_path_within_root(rel.string(), root);
|
||||
}
|
||||
|
||||
bool is_safe_to_open_file_name(const std::string &file_name)
|
||||
{
|
||||
// Formats that cannot carry macros or scripts. Legacy and OpenDocument office files, HTML and SVG are left out on purpose.
|
||||
static const std::vector<std::string> safe_extensions = {
|
||||
"jpg", "jpeg", "jfif", "pjpeg", "pjp", "png", "gif", "bmp", "webp", "tif", "tiff",
|
||||
"pdf", "txt", "md", "csv", "docx", "xlsx", "pptx",
|
||||
"stl", "obj", "3mf", "amf", "ply", "step", "stp", "iges", "igs", "dxf",
|
||||
"mp4", "mov", "webm"};
|
||||
// The name must end in the extension itself: Windows drops trailing dots and spaces and reads ':' as a stream separator.
|
||||
const size_t dot = file_name.find_last_of('.');
|
||||
if (dot == std::string::npos || file_name.find_first_of("/\\:") != std::string::npos)
|
||||
return false;
|
||||
const std::string extension = boost::algorithm::to_lower_copy(file_name.substr(dot + 1));
|
||||
return std::find(safe_extensions.begin(), safe_extensions.end(), extension) != safe_extensions.end();
|
||||
}
|
||||
|
||||
bool is_img_file(const std::string &path)
|
||||
{
|
||||
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
||||
@@ -1321,6 +1359,31 @@ unsigned get_current_pid()
|
||||
#endif
|
||||
}
|
||||
|
||||
boost::filesystem::path download_marker_path(const boost::filesystem::path &dest_folder, const std::string &filename)
|
||||
{
|
||||
return dest_folder / (filename + "." + std::to_string(get_current_pid()) + ".download");
|
||||
}
|
||||
|
||||
bool find_unused_filename(const boost::filesystem::path &dest_folder, const std::string &filename,
|
||||
const boost::filesystem::path &ignored_marker, std::string &result)
|
||||
{
|
||||
// Probe the name that will be written, so a name the sanitizing maps onto an existing file is versioned too.
|
||||
const std::string sanitized = sanitize_filename(filename);
|
||||
const std::string extension = boost::filesystem::path(sanitized).extension().string();
|
||||
const std::string stem = sanitized.substr(0, sanitized.size() - extension.size());
|
||||
auto is_used = [&](const std::string &name) {
|
||||
const boost::filesystem::path marker = download_marker_path(dest_folder, name);
|
||||
return boost::filesystem::exists(dest_folder / name) || (marker != ignored_marker && boost::filesystem::exists(marker));
|
||||
};
|
||||
result = sanitized;
|
||||
for (size_t version = 1; is_used(result); ++version) {
|
||||
if (version > 999)
|
||||
return false;
|
||||
result = stem + "(" + std::to_string(version) + ")" + extension;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string per_user_temp_id()
|
||||
{
|
||||
#ifdef WIN32
|
||||
|
||||
@@ -1028,14 +1028,9 @@ if (UNIX AND NOT APPLE)
|
||||
find_package(GTK${SLIC3R_GTK} REQUIRED)
|
||||
pkg_check_modules(LIBSECRET REQUIRED libsecret-1)
|
||||
pkg_check_modules(webkit2gtk REQUIRED webkit2gtk-4.1)
|
||||
if (FLATPAK)
|
||||
# I don't know why this is needed, but for whatever reason slic3r isn't
|
||||
# linking to X11 and webkit2gtk. force it.
|
||||
find_package(X11 REQUIRED)
|
||||
target_link_libraries(libslic3r_gui ${X11_LIBRARIES} ${webkit2gtk_LIBRARIES})
|
||||
endif()
|
||||
find_package(X11 REQUIRED)
|
||||
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GTK${SLIC3R_GTK}_INCLUDE_DIRS} ${LIBSECRET_INCLUDE_DIRS} ${webkit2gtk_INCLUDE_DIRS})
|
||||
target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES} fontconfig ${LIBSECRET_LIBRARIES})
|
||||
target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES} fontconfig ${LIBSECRET_LIBRARIES} ${webkit2gtk_LIBRARIES} ${X11_LIBRARIES})
|
||||
|
||||
# Propagate GDK backend detection results as compile definitions so that
|
||||
# LinuxDisplayBackend.cpp can include the right GDK headers.
|
||||
|
||||
@@ -162,6 +162,14 @@ ColorRGBA GLVolume::SUPPORT_BLOCKER_COL = {1.0f, 0.3f, 0.3f, 0.4f};
|
||||
|
||||
ColorRGBA GLVolume::MODEL_HIDDEN_COL = {0.f, 0.f, 0.f, 0.3f};
|
||||
|
||||
// Precise Seam modifier colors
|
||||
ColorRGBA GLVolume::PRECISE_SEAM_CENTER_COL = {1.0f, 0.627f, 0.082f, 0.6f}; // FFA015 - orange
|
||||
ColorRGBA GLVolume::PRECISE_SEAM_LEFT_COL = {1.0f, 0.753f, 0.0f, 0.6f}; // FFC000 - golden
|
||||
ColorRGBA GLVolume::PRECISE_SEAM_RIGHT_COL = {1.0f, 0.514f, 0.0f, 0.6f}; // FF8300 - dark orange
|
||||
ColorRGBA GLVolume::PRECISE_SEAM_ENFORCED_COL = {0.412f, 0.820f, 0.412f, 0.6f}; // 69D169 - green
|
||||
ColorRGBA GLVolume::PRECISE_SEAM_NEUTRAL_COL = {0.655f, 0.655f, 0.655f, 0.6f}; // A7A7A7 - gray
|
||||
ColorRGBA GLVolume::PRECISE_SEAM_BLOCKED_COL = {0.820f, 0.412f, 0.412f, 0.6f}; // D16969 - red
|
||||
|
||||
std::array<ColorRGBA, 5> GLVolume::MODEL_COLOR = { {
|
||||
{ 1.0f, 1.0f, 0.0f, 1.f },
|
||||
{ 1.0f, 0.5f, 0.5f, 1.f },
|
||||
@@ -363,6 +371,28 @@ ColorRGBA color_from_model_volume(const ModelVolume& model_volume)
|
||||
ColorRGBA color;
|
||||
if (model_volume.is_negative_volume())
|
||||
return GLVolume::MODEL_NEGTIVE_COL;
|
||||
else if (model_volume.is_precise_seam()) {
|
||||
// Return color based on Precise Seam subtype.
|
||||
// Exhaustive switch (no default) so -Wswitch flags any future PRECISE_SEAM_* additions.
|
||||
switch (model_volume.type()) {
|
||||
case ModelVolumeType::PRECISE_SEAM_CENTER: return GLVolume::PRECISE_SEAM_CENTER_COL;
|
||||
case ModelVolumeType::PRECISE_SEAM_LEFT: return GLVolume::PRECISE_SEAM_LEFT_COL;
|
||||
case ModelVolumeType::PRECISE_SEAM_RIGHT: return GLVolume::PRECISE_SEAM_RIGHT_COL;
|
||||
case ModelVolumeType::PRECISE_SEAM_ENFORCED: return GLVolume::PRECISE_SEAM_ENFORCED_COL;
|
||||
case ModelVolumeType::PRECISE_SEAM_NEUTRAL: return GLVolume::PRECISE_SEAM_NEUTRAL_COL;
|
||||
case ModelVolumeType::PRECISE_SEAM_BLOCKED: return GLVolume::PRECISE_SEAM_BLOCKED_COL;
|
||||
// Non-seam types are unreachable due to the outer is_precise_seam() guard;
|
||||
// listed explicitly so this switch stays exhaustive over ModelVolumeType.
|
||||
case ModelVolumeType::INVALID:
|
||||
case ModelVolumeType::MODEL_PART:
|
||||
case ModelVolumeType::NEGATIVE_VOLUME:
|
||||
case ModelVolumeType::PARAMETER_MODIFIER:
|
||||
case ModelVolumeType::SUPPORT_BLOCKER:
|
||||
case ModelVolumeType::SUPPORT_ENFORCER:
|
||||
break;
|
||||
}
|
||||
return GLVolume::MODEL_MIDIFIER_COL; // unreachable fallback
|
||||
}
|
||||
else if (model_volume.is_modifier())
|
||||
#if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT
|
||||
return GLVolume::MODEL_MIDIFIER_COL;
|
||||
|
||||
@@ -93,6 +93,13 @@ public:
|
||||
static ColorRGBA SUPPORT_ENFORCER_COL;
|
||||
static ColorRGBA SUPPORT_BLOCKER_COL;
|
||||
static ColorRGBA MODEL_HIDDEN_COL;
|
||||
// Precise Seam modifier colors
|
||||
static ColorRGBA PRECISE_SEAM_CENTER_COL;
|
||||
static ColorRGBA PRECISE_SEAM_LEFT_COL;
|
||||
static ColorRGBA PRECISE_SEAM_RIGHT_COL;
|
||||
static ColorRGBA PRECISE_SEAM_ENFORCED_COL;
|
||||
static ColorRGBA PRECISE_SEAM_NEUTRAL_COL;
|
||||
static ColorRGBA PRECISE_SEAM_BLOCKED_COL;
|
||||
|
||||
static void update_render_colors();
|
||||
static void load_render_colors();
|
||||
|
||||
@@ -428,7 +428,7 @@ void AuFile::on_dclick(wxMouseEvent &evt)
|
||||
if (m_type == AddFileButton)
|
||||
return;
|
||||
else
|
||||
wxLaunchDefaultApplication(m_file_path.wstring(), 0);
|
||||
desktop_open_project_attachment(this, m_file_path);
|
||||
}
|
||||
|
||||
void AuFile::on_mouse_left_up(wxMouseEvent &evt)
|
||||
|
||||
@@ -818,12 +818,14 @@ NewCalibrationHistoryDialog::NewCalibrationHistoryDialog(wxWindow *parent, const
|
||||
if (support_nozzle_volume(curr_obj)) {
|
||||
Label *nozzle_name_title = new Label(top_panel, _L("Nozzle"));
|
||||
m_comboBox_nozzle_type = new ::ComboBox(top_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, NEW_HISTORY_DIALOG_INPUT_SIZE, 0, nullptr, wxCB_READONLY);
|
||||
wxArrayString nozzle_items;
|
||||
// The labels are in display order, not enum order (E3D High Flow is 5 but the fifth label), so each
|
||||
// item carries its NozzleVolumeType as client data.
|
||||
const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type");
|
||||
if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) {
|
||||
for (auto item : nozzle_volume_type_def->enum_labels) { nozzle_items.push_back(_L(item)); }
|
||||
for (size_t i = 0; i < nozzle_volume_type_def->enum_labels.size(); ++i)
|
||||
m_comboBox_nozzle_type->Append(_L(nozzle_volume_type_def->enum_labels[i]), wxNullBitmap,
|
||||
(void *) (intptr_t) nozzle_volume_type_def->enum_keys_map->at(nozzle_volume_type_def->enum_values[i]));
|
||||
}
|
||||
m_comboBox_nozzle_type->Set(nozzle_items);
|
||||
m_comboBox_nozzle_type->SetSelection(-1);
|
||||
flex_sizer->Add(nozzle_name_title);
|
||||
flex_sizer->Add(m_comboBox_nozzle_type);
|
||||
@@ -891,7 +893,7 @@ int NewCalibrationHistoryDialog::get_nozzle_combo_id_code() const
|
||||
|
||||
void NewCalibrationHistoryDialog::on_select_nozzle_pos(wxCommandEvent &event)
|
||||
{
|
||||
// Mirror the picked hotend's flow onto the (Orca index-based) nozzle-type combo.
|
||||
// Mirror the picked hotend's flow onto the nozzle-type combo.
|
||||
if (!curr_obj || !m_comboBox_nozzle_id || !m_comboBox_nozzle_type || !curr_obj->GetNozzleSystem())
|
||||
return;
|
||||
|
||||
@@ -902,7 +904,9 @@ void NewCalibrationHistoryDialog::on_select_nozzle_pos(wxCommandEvent &event)
|
||||
DevNozzle nozzle = curr_obj->GetNozzleSystem()->GetNozzleByPosId(pos);
|
||||
if (nozzle.IsNormal()) {
|
||||
NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.GetNozzleFlowType());
|
||||
m_comboBox_nozzle_type->SetSelection(static_cast<int>(volume_type));
|
||||
for (unsigned int i = 0; i < m_comboBox_nozzle_type->GetCount(); ++i)
|
||||
if (NozzleVolumeType(intptr_t(m_comboBox_nozzle_type->GetClientData(i))) == volume_type)
|
||||
m_comboBox_nozzle_type->SetSelection(i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,7 +953,7 @@ void NewCalibrationHistoryDialog::on_ok(wxCommandEvent &event)
|
||||
msg_dlg.ShowModal();
|
||||
return;
|
||||
}
|
||||
m_new_result.nozzle_volume_type = NozzleVolumeType(m_comboBox_nozzle_type->GetSelection());
|
||||
m_new_result.nozzle_volume_type = NozzleVolumeType(intptr_t(m_comboBox_nozzle_type->GetClientData(m_comboBox_nozzle_type->GetSelection())));
|
||||
}
|
||||
|
||||
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
||||
|
||||
@@ -805,6 +805,29 @@ void CalibrationPresetPage::create_selection_panel(wxWindow* parent)
|
||||
#define NOZZLE_LIST_DEFAULT 1
|
||||
float nozzle_diameter_list[NOZZLE_LIST_COUNT] = {0.2, 0.4, 0.6, 0.8 };
|
||||
|
||||
// The nozzle_volume_type labels are in display order, not enum order (E3D High Flow is 5 but the fifth
|
||||
// label), so each item carries its NozzleVolumeType as client data and is selected by that value.
|
||||
static void select_nozzle_volume(ComboBox *combo, NozzleVolumeType volume_type)
|
||||
{
|
||||
for (unsigned int i = 0; i < combo->GetCount(); ++i)
|
||||
if (NozzleVolumeType(intptr_t(combo->GetClientData(i))) == volume_type) {
|
||||
combo->SetSelection(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void fill_nozzle_volume_combo(ComboBox *combo)
|
||||
{
|
||||
combo->Clear();
|
||||
const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type");
|
||||
if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) {
|
||||
for (size_t i = 0; i < nozzle_volume_type_def->enum_labels.size(); ++i)
|
||||
combo->Append(_L(nozzle_volume_type_def->enum_labels[i]), wxNullBitmap,
|
||||
(void *) (intptr_t) nozzle_volume_type_def->enum_keys_map->at(nozzle_volume_type_def->enum_values[i]));
|
||||
}
|
||||
select_nozzle_volume(combo, NozzleVolumeType::nvtStandard);
|
||||
}
|
||||
|
||||
void CalibrationPresetPage::init_selection_values()
|
||||
{
|
||||
// init nozzle diameter and nozzle volume
|
||||
@@ -815,15 +838,7 @@ void CalibrationPresetPage::init_selection_values()
|
||||
}
|
||||
m_comboBox_nozzle_dia->SetSelection(NOZZLE_LIST_DEFAULT);
|
||||
|
||||
m_comboBox_nozzle_volume->Clear();
|
||||
const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type");
|
||||
if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) {
|
||||
for (auto item : nozzle_volume_type_def->enum_labels) {
|
||||
m_comboBox_nozzle_volume->AppendString(_L(item));
|
||||
}
|
||||
}
|
||||
|
||||
m_comboBox_nozzle_volume->SetSelection(int(NozzleVolumeType::nvtStandard));
|
||||
fill_nozzle_volume_combo(m_comboBox_nozzle_volume);
|
||||
}
|
||||
|
||||
Preset* cur_printer_preset = get_printer_preset(curr_obj, 0.4);
|
||||
@@ -868,15 +883,7 @@ void CalibrationPresetPage::init_selection_values()
|
||||
}
|
||||
m_left_comboBox_nozzle_dia->SetSelection(NOZZLE_LIST_DEFAULT);
|
||||
|
||||
m_left_comboBox_nozzle_volume->Clear();
|
||||
const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type");
|
||||
if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) {
|
||||
for (auto item : nozzle_volume_type_def->enum_labels) {
|
||||
m_left_comboBox_nozzle_volume->AppendString(_L(item));
|
||||
}
|
||||
}
|
||||
|
||||
m_left_comboBox_nozzle_volume->SetSelection(int(NozzleVolumeType::nvtStandard));
|
||||
fill_nozzle_volume_combo(m_left_comboBox_nozzle_volume);
|
||||
}
|
||||
|
||||
// right
|
||||
@@ -887,15 +894,7 @@ void CalibrationPresetPage::init_selection_values()
|
||||
}
|
||||
m_right_comboBox_nozzle_dia->SetSelection(NOZZLE_LIST_DEFAULT);
|
||||
|
||||
m_right_comboBox_nozzle_volume->Clear();
|
||||
const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type");
|
||||
if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) {
|
||||
for (auto item : nozzle_volume_type_def->enum_labels) {
|
||||
m_right_comboBox_nozzle_volume->AppendString(_L(item));
|
||||
}
|
||||
}
|
||||
|
||||
m_right_comboBox_nozzle_volume->SetSelection(int(NozzleVolumeType::nvtStandard));
|
||||
fill_nozzle_volume_combo(m_right_comboBox_nozzle_volume);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -982,13 +981,13 @@ NozzleVolumeType CalibrationPresetPage::get_nozzle_volume_type(int extruder_id)
|
||||
if (curr_obj) {
|
||||
if (curr_obj->is_multi_extruders()) {
|
||||
if (extruder_id == LEFT_EXTRUDER_ID) {
|
||||
return NozzleVolumeType(m_left_comboBox_nozzle_volume->GetSelection());
|
||||
return NozzleVolumeType(intptr_t(m_left_comboBox_nozzle_volume->GetClientData(m_left_comboBox_nozzle_volume->GetSelection())));
|
||||
} else if (extruder_id == RIGHT_EXTRUDER_ID) {
|
||||
return NozzleVolumeType(m_right_comboBox_nozzle_volume->GetSelection());
|
||||
return NozzleVolumeType(intptr_t(m_right_comboBox_nozzle_volume->GetClientData(m_right_comboBox_nozzle_volume->GetSelection())));
|
||||
}
|
||||
}
|
||||
else
|
||||
return NozzleVolumeType(m_comboBox_nozzle_volume->GetSelection());
|
||||
return NozzleVolumeType(intptr_t(m_comboBox_nozzle_volume->GetClientData(m_comboBox_nozzle_volume->GetSelection())));
|
||||
}
|
||||
return NozzleVolumeType::nvtStandard;
|
||||
}
|
||||
@@ -2126,7 +2125,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj)
|
||||
}
|
||||
|
||||
if (obj->GetExtderSystem()->GetNozzleFlowType(i) != NozzleFlowType::NONE_FLOWTYPE) {
|
||||
m_left_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i))));
|
||||
select_nozzle_volume(m_left_comboBox_nozzle_volume, DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i)));
|
||||
} else {
|
||||
m_left_comboBox_nozzle_volume->SetSelection(0);
|
||||
}
|
||||
@@ -2146,7 +2145,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj)
|
||||
}
|
||||
|
||||
if (obj->GetExtderSystem()->GetNozzleFlowType(i) != NozzleFlowType::NONE_FLOWTYPE) {
|
||||
m_right_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i))));
|
||||
select_nozzle_volume(m_right_comboBox_nozzle_volume, DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i)));
|
||||
} else {
|
||||
m_right_comboBox_nozzle_volume->SetSelection(0);
|
||||
}
|
||||
@@ -2184,7 +2183,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj)
|
||||
else {
|
||||
if ((obj->GetExtderSystem()->GetTotalExtderCount() > 0) && (obj->GetExtderSystem()->GetNozzleFlowType(0) != NozzleFlowType::NONE_FLOWTYPE))
|
||||
{
|
||||
m_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0))));
|
||||
select_nozzle_volume(m_comboBox_nozzle_volume, DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0)));
|
||||
} else {
|
||||
m_comboBox_nozzle_volume->SetSelection(0);
|
||||
}
|
||||
|
||||
@@ -732,6 +732,7 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector<
|
||||
case NozzleVolumeType::nvtStandard: nozzle_id_str += _L("Standard Flow"); break;
|
||||
case NozzleVolumeType::nvtHighFlow: nozzle_id_str += _L("High Flow"); break;
|
||||
case NozzleVolumeType::nvtTPUHighFlow: nozzle_id_str += _L("TPU High Flow"); break;
|
||||
case NozzleVolumeType::nvtE3DHighFlow: nozzle_id_str += _L("E3D High Flow"); break;
|
||||
default: break;
|
||||
}
|
||||
nozzle_id_value->SetLabel(nozzle_id_str);
|
||||
|
||||
@@ -171,14 +171,16 @@ void ConfigManipulation::check_adaptive_pressure_advance_model(DynamicPrintConfi
|
||||
return;
|
||||
|
||||
const auto* model = config->option<ConfigOptionStrings>("adaptive_pressure_advance_model");
|
||||
if (model == nullptr || model->values.empty())
|
||||
if (model == nullptr)
|
||||
return;
|
||||
|
||||
std::string raw_model;
|
||||
for (const std::string& chunk : model->values)
|
||||
raw_model += chunk;
|
||||
|
||||
std::string error = AdaptivePAProcessor::validate_adaptive_pa_model(raw_model);
|
||||
// Each extruder variant holds its own model.
|
||||
std::string error;
|
||||
for (const std::string& variant_model : model->values) {
|
||||
error = AdaptivePAProcessor::validate_adaptive_pa_model(variant_model);
|
||||
if (!error.empty())
|
||||
break;
|
||||
}
|
||||
if (!error.empty()) {
|
||||
wxString msg_text = _L("Adaptive Pressure Advance model validation failed:\n");
|
||||
msg_text += from_u8(error);
|
||||
|
||||
@@ -36,7 +36,8 @@ static NozzleVolumeType convert_to_nozzle_type(const std::string &str)
|
||||
return NozzleVolumeType::nvtHighFlow;
|
||||
else if (str[1] == 'U')
|
||||
return NozzleVolumeType::nvtTPUHighFlow;
|
||||
// Orca: no nvtE3DHighFlow in Orca's NozzleVolumeType; map 'B' to Standard
|
||||
else if (str[1] == 'B')
|
||||
return NozzleVolumeType::nvtE3DHighFlow;
|
||||
else
|
||||
return NozzleVolumeType::nvtStandard;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
namespace Slic3r
|
||||
{
|
||||
|
||||
// ---- DevNozzle: flow/volume conversions (Standard / High Flow / TPU High Flow) ----------------------
|
||||
// ---- DevNozzle: flow/volume conversions (Standard / High Flow / TPU High Flow / E3D High Flow) ------
|
||||
// Device-reported U_FLOW nozzles map to nvtTPUHighFlow so a synced TPU-HF rack activates the H2C
|
||||
// change_filament_gcode TPU-kit branch. nvtHybrid is a slicer-only sentinel with no device
|
||||
// representation, so ToNozzleFlowType(nvtHybrid) falls through to NONE_FLOWTYPE.
|
||||
@@ -24,6 +24,7 @@ NozzleFlowType DevNozzle::ToNozzleFlowType(const NozzleVolumeType& type)
|
||||
case NozzleVolumeType::nvtStandard: return NozzleFlowType::S_FLOW;
|
||||
case NozzleVolumeType::nvtHighFlow: return NozzleFlowType::H_FLOW;
|
||||
case NozzleVolumeType::nvtTPUHighFlow: return NozzleFlowType::U_FLOW;
|
||||
case NozzleVolumeType::nvtE3DHighFlow: return NozzleFlowType::E_FLOW;
|
||||
default: return NozzleFlowType::NONE_FLOWTYPE;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +35,7 @@ NozzleVolumeType DevNozzle::ToNozzleVolumeType(const NozzleFlowType& type)
|
||||
case NozzleFlowType::S_FLOW: return NozzleVolumeType::nvtStandard;
|
||||
case NozzleFlowType::H_FLOW: return NozzleVolumeType::nvtHighFlow;
|
||||
case NozzleFlowType::U_FLOW: return NozzleVolumeType::nvtTPUHighFlow;
|
||||
case NozzleFlowType::E_FLOW: return NozzleVolumeType::nvtE3DHighFlow;
|
||||
default: {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "nozzle flow type None convert to nozzle volume type Standard";
|
||||
return NozzleVolumeType::nvtStandard;
|
||||
@@ -47,6 +49,7 @@ wxString DevNozzle::GetNozzleFlowTypeStr(NozzleFlowType type)
|
||||
case NozzleFlowType::H_FLOW: return _L("High Flow");
|
||||
case NozzleFlowType::S_FLOW: return _L("Standard");
|
||||
case NozzleFlowType::U_FLOW: return _L("TPU High Flow");
|
||||
case NozzleFlowType::E_FLOW: return _L("E3D High Flow");
|
||||
default: break;
|
||||
}
|
||||
|
||||
@@ -61,6 +64,7 @@ std::string DevNozzle::GetNozzleFlowTypeString(NozzleFlowType type)
|
||||
case NozzleFlowType::H_FLOW: return "High Flow";
|
||||
case NozzleFlowType::S_FLOW: return "Standard";
|
||||
case NozzleFlowType::U_FLOW: return "TPU High Flow";
|
||||
case NozzleFlowType::E_FLOW: return "E3D High Flow";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
@@ -73,6 +77,7 @@ std::string DevNozzle::ToNozzleFlowString(const NozzleFlowType& type)
|
||||
case NozzleFlowType::S_FLOW: return "Standard";
|
||||
case NozzleFlowType::H_FLOW: return "High Flow";
|
||||
case NozzleFlowType::U_FLOW: return "TPU High Flow";
|
||||
case NozzleFlowType::E_FLOW: return "E3D High Flow";
|
||||
default: return std::string();
|
||||
}
|
||||
}
|
||||
@@ -408,6 +413,7 @@ static unordered_map<string, NozzleFlowType> _str2_nozzle_flow_type = {
|
||||
{"X", NozzleFlowType::S_FLOW},
|
||||
{"E", NozzleFlowType::H_FLOW}, // E3D high-flow
|
||||
{"U", NozzleFlowType::U_FLOW}, // TPU 1.75 high-flow -> nvtTPUHighFlow
|
||||
{"B", NozzleFlowType::E_FLOW}, // E3D High Flow -> nvtE3DHighFlow
|
||||
};
|
||||
|
||||
static unordered_map<string, NozzleType> _str2_nozzle_type = {
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
|
||||
// Device flow-type mapping note (nozzle rack): the device U_FLOW value maps to nvtTPUHighFlow
|
||||
// (TPU High Flow), so a device-synced TPU-HF rack resolves to nvtTPUHighFlow and the H2C
|
||||
// change_filament_gcode TPU-kit branch can activate. nvtHybrid is a slicer-only sentinel with no
|
||||
// device representation, so it stays out of these device flow-type conversions.
|
||||
// change_filament_gcode TPU-kit branch can activate. E_FLOW maps to nvtE3DHighFlow. nvtHybrid is a
|
||||
// slicer-only sentinel with no device representation, so it stays out of these device flow-type
|
||||
// conversions.
|
||||
//
|
||||
// GetExtruderNozzleInfo (and its ExtruderNozzleInfos aggregate) is intentionally omitted here;
|
||||
// the SelectMachine slicing-vs-installed nozzle comparison collects its per-extruder NozzleDef
|
||||
@@ -36,13 +37,13 @@ namespace Slic3r
|
||||
float m_diameter = 0.0f;// unknown until reported by the printer
|
||||
|
||||
public:
|
||||
// flow/volume conversions (Standard / High Flow / TPU High Flow)
|
||||
// flow/volume conversions (Standard / High Flow / TPU High Flow / E3D High Flow)
|
||||
static NozzleFlowType ToNozzleFlowType(const NozzleVolumeType& type);
|
||||
static NozzleVolumeType ToNozzleVolumeType(const NozzleFlowType& type);
|
||||
|
||||
static wxString GetNozzleFlowTypeStr(NozzleFlowType type);
|
||||
static std::string GetNozzleFlowTypeString(NozzleFlowType type);// untranslated literal ("High Flow"/"Standard"/"TPU High Flow") — the filament blacklist JSON matches these raw strings, so it must NOT use the translated GetNozzleFlowTypeStr (would break non-English locales)
|
||||
static std::string ToNozzleFlowString(const NozzleFlowType& type);// untranslated "Standard"/"High Flow"/"TPU High Flow" ("" for none) — the raw literal serialized into the get_auto_nozzle_mapping payload
|
||||
static std::string GetNozzleFlowTypeString(NozzleFlowType type);// untranslated literal ("High Flow"/"Standard"/"TPU High Flow"/"E3D High Flow") — the filament blacklist JSON matches these raw strings, so it must NOT use the translated GetNozzleFlowTypeStr (would break non-English locales)
|
||||
static std::string ToNozzleFlowString(const NozzleFlowType& type);// untranslated "Standard"/"High Flow"/"TPU High Flow"/"E3D High Flow" ("" for none) — the raw literal serialized into the get_auto_nozzle_mapping payload
|
||||
static wxString GetNozzleTypeStr(NozzleType type);
|
||||
|
||||
public:
|
||||
|
||||
@@ -350,6 +350,10 @@ static wxString _generate_nozzle_id(NozzleVolumeType nozzle_type, const std::str
|
||||
nozzle_id += "H";
|
||||
break;
|
||||
}
|
||||
case NozzleVolumeType::nvtE3DHighFlow: {
|
||||
nozzle_id += "B";
|
||||
break;
|
||||
}
|
||||
default:
|
||||
nozzle_id += "H";
|
||||
break;
|
||||
@@ -371,6 +375,8 @@ NozzleVolumeType convert_to_nozzle_type(const std::string &str)
|
||||
res = NozzleVolumeType::nvtStandard;
|
||||
else if (str[1] == 'H')
|
||||
res = NozzleVolumeType::nvtHighFlow;
|
||||
else if (str[1] == 'B')
|
||||
res = NozzleVolumeType::nvtE3DHighFlow;
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -4918,7 +4924,8 @@ void MachineObject::update_slice_info(std::string project_id, std::string profil
|
||||
std::string subtask_json;
|
||||
unsigned http_code = 0;
|
||||
std::string http_body;
|
||||
if (m_agent->get_subtask_info(subtask_id, &subtask_json, &http_code, &http_body) == 0) {
|
||||
if (m_agent->get_subtask_info(subtask_id, &subtask_json, &http_code, &http_body,
|
||||
Slic3r::GUI::wxGetApp().get_printer_cloud_provider()) == 0) {
|
||||
try {
|
||||
if (!subtask_json.empty()) {
|
||||
|
||||
|
||||
@@ -71,17 +71,6 @@ bool FileGet::is_subdomain(const std::string& url, const std::string& domain)
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
unsigned get_current_pid()
|
||||
{
|
||||
#ifdef WIN32
|
||||
return GetCurrentProcessId();
|
||||
#else
|
||||
return ::getpid();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// int = DOWNLOAD ID; string = file path
|
||||
wxDEFINE_EVENT(EVT_DWNLDR_FILE_COMPLETE, wxCommandEvent);
|
||||
// int = DOWNLOAD ID; string = error msg
|
||||
@@ -144,25 +133,10 @@ void FileGet::priv::get_perform()
|
||||
std::string extension;
|
||||
if (m_written == 0)
|
||||
{
|
||||
boost::filesystem::path dest_path = m_dest_folder / m_filename;
|
||||
extension = dest_path.extension().string();
|
||||
std::string just_filename = m_filename.substr(0, m_filename.size() - extension.size());
|
||||
std::string final_filename = just_filename;
|
||||
// Find unsed filename
|
||||
std::string final_filename;
|
||||
bool found = false;
|
||||
try {
|
||||
size_t version = 0;
|
||||
while (boost::filesystem::exists(m_dest_folder / (final_filename + extension)) || boost::filesystem::exists(m_dest_folder / (final_filename + extension + "." + std::to_string(get_current_pid()) + ".download")))
|
||||
{
|
||||
++version;
|
||||
if (version > 999) {
|
||||
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
|
||||
evt->SetString(GUI::format_wxstr(L"Failed to find suitable filename. Last name: %1%." , (m_dest_folder / (final_filename + extension)).string()));
|
||||
evt->SetInt(m_id);
|
||||
m_evt_handler->QueueEvent(evt);
|
||||
return;
|
||||
}
|
||||
final_filename = GUI::format("%1%(%2%)", just_filename, std::to_string(version));
|
||||
}
|
||||
found = find_unused_filename(m_dest_folder, m_filename, m_tmp_path, final_filename);
|
||||
} catch (const boost::filesystem::filesystem_error& e)
|
||||
{
|
||||
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
|
||||
@@ -171,10 +145,18 @@ void FileGet::priv::get_perform()
|
||||
m_evt_handler->QueueEvent(evt);
|
||||
return;
|
||||
}
|
||||
if (!found) {
|
||||
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
|
||||
evt->SetString(GUI::format_wxstr(L"Failed to find suitable filename. Last name: %1%." , (m_dest_folder / final_filename).string()));
|
||||
evt->SetInt(m_id);
|
||||
m_evt_handler->QueueEvent(evt);
|
||||
return;
|
||||
}
|
||||
|
||||
m_filename = sanitize_filename(final_filename + extension);
|
||||
m_filename = final_filename;
|
||||
extension = boost::filesystem::path(m_filename).extension().string();
|
||||
|
||||
m_tmp_path = m_dest_folder / (m_filename + "." + std::to_string(get_current_pid()) + ".download");
|
||||
m_tmp_path = download_marker_path(m_dest_folder, m_filename);
|
||||
|
||||
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE);
|
||||
evt->SetString(boost::nowide::widen(m_filename));
|
||||
@@ -221,7 +203,32 @@ void FileGet::priv::get_perform()
|
||||
if(dest_path.empty()) {
|
||||
std::string filename = extract_remote_filename(header);
|
||||
if (!filename.empty()) {
|
||||
m_filename = filename;
|
||||
// The name comes from the server: keep it inside the destination folder and never
|
||||
// replace an existing file. Keep the current name if nothing usable remains.
|
||||
filename = sanitize_file_basename(filename);
|
||||
std::string unused;
|
||||
try {
|
||||
if (filename.empty() || !find_unused_filename(m_dest_folder, filename, m_tmp_path, unused))
|
||||
unused.clear();
|
||||
} catch (const boost::filesystem::filesystem_error&) {
|
||||
unused.clear();
|
||||
}
|
||||
const boost::filesystem::path tmp_path = unused.empty() ? m_tmp_path : download_marker_path(m_dest_folder, unused);
|
||||
if (tmp_path != m_tmp_path) {
|
||||
// Move the marker to the adopted name so that other downloads see the name as taken.
|
||||
// Only before anything is written, so that no downloaded data has to be carried over.
|
||||
FILE* tmp_file = m_written == 0 ? fopen(wxString(tmp_path.wstring()).c_str(), "wb") : nullptr;
|
||||
if (tmp_file != nullptr) {
|
||||
fclose(file);
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(m_tmp_path, ec);
|
||||
file = tmp_file;
|
||||
m_tmp_path = tmp_path;
|
||||
} else
|
||||
unused.clear();
|
||||
}
|
||||
if (!unused.empty())
|
||||
m_filename = unused;
|
||||
dest_path = m_dest_folder / m_filename;
|
||||
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE);
|
||||
evt->SetString(boost::nowide::widen(m_filename));
|
||||
@@ -327,6 +334,18 @@ void FileGet::priv::get_perform()
|
||||
m_evt_handler->QueueEvent(evt);
|
||||
}
|
||||
fclose(file);
|
||||
// Another file may have taken the name while downloading.
|
||||
if (!dest_path.empty() && boost::filesystem::exists(dest_path)) {
|
||||
std::string unused;
|
||||
if (!find_unused_filename(m_dest_folder, m_filename, m_tmp_path, unused))
|
||||
throw std::runtime_error("No unused file name.");
|
||||
m_filename = unused;
|
||||
dest_path = m_dest_folder / m_filename;
|
||||
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE);
|
||||
evt->SetString(boost::nowide::widen(m_filename));
|
||||
evt->SetInt(m_id);
|
||||
m_evt_handler->QueueEvent(evt);
|
||||
}
|
||||
boost::filesystem::rename(m_tmp_path, dest_path);
|
||||
}
|
||||
catch (const std::exception& /*e*/)
|
||||
|
||||
@@ -566,7 +566,8 @@ std::vector<int> SeparatedDragDropPanel::GetHighFlowFilaments() const
|
||||
const int right_eid = 1;
|
||||
if (nozzle_volumes->values.size() > right_eid) {
|
||||
int volume_type = nozzle_volumes->values[right_eid];
|
||||
if (volume_type == static_cast<int>(NozzleVolumeType::nvtHighFlow)) {
|
||||
if (volume_type == static_cast<int>(NozzleVolumeType::nvtHighFlow) ||
|
||||
volume_type == static_cast<int>(NozzleVolumeType::nvtE3DHighFlow)) {
|
||||
return m_unified_panel->GetAllFilaments();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +185,12 @@ std::vector<int> FilamentMapManualPanel::GetFilamentVolumeMaps() const
|
||||
auto preset_bundle = wxGetApp().preset_bundle;
|
||||
auto proj_config = preset_bundle->project_config;
|
||||
auto nozzle_volume_values = proj_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")->values;
|
||||
// The high-flow panel of the right extruder also holds an E3D High Flow nozzle; keep that type
|
||||
// instead of writing plain High Flow, which the extruder does not have.
|
||||
const int right_extruder_id = 1;
|
||||
const bool right_e3d_high_flow = nozzle_volume_values.size() > right_extruder_id &&
|
||||
nozzle_volume_values[right_extruder_id] == static_cast<int>(NozzleVolumeType::nvtE3DHighFlow);
|
||||
const int right_high_flow_volume = static_cast<int>(right_e3d_high_flow ? NozzleVolumeType::nvtE3DHighFlow : NozzleVolumeType::nvtHighFlow);
|
||||
|
||||
for (int i = 0; i < (int) volume_map.size(); ++i) {
|
||||
int filament_id = i + 1;
|
||||
@@ -199,7 +205,7 @@ std::vector<int> FilamentMapManualPanel::GetFilamentVolumeMaps() const
|
||||
}
|
||||
}
|
||||
else if (std::find(right_high_flow_filaments.begin(), right_high_flow_filaments.end(), filament_id) != right_high_flow_filaments.end()) {
|
||||
volume_map[i] = static_cast<int>(NozzleVolumeType::nvtHighFlow);
|
||||
volume_map[i] = right_high_flow_volume;
|
||||
}
|
||||
else if (std::find(right_standard_filaments.begin(), right_standard_filaments.end(), filament_id) != right_standard_filaments.end()) {
|
||||
volume_map[i] = static_cast<int>(NozzleVolumeType::nvtStandard);
|
||||
|
||||
@@ -6546,8 +6546,11 @@ void GLCanvas3D::render_thumbnail_internal(ThumbnailData& thumbnail_data, const
|
||||
// glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("render_thumbnail: finished");
|
||||
|
||||
// Puts the canvas viewport back in place of the thumbnail one set above.
|
||||
wxGetApp().plater()->get_camera().apply_viewport();
|
||||
// Puts the canvas viewport back in place of the thumbnail one set above. The CLI renders
|
||||
// thumbnails with no application and no plater, so there is no canvas viewport to restore.
|
||||
if (wxTheApp != nullptr)
|
||||
if (Plater *plater = wxGetApp().plater(); plater != nullptr)
|
||||
plater->get_camera().apply_viewport();
|
||||
}
|
||||
|
||||
void GLCanvas3D::render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params,
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/any.hpp>
|
||||
|
||||
#include <wx/filename.h>
|
||||
#include <wx/filesys.h>
|
||||
|
||||
#if __APPLE__
|
||||
#import <IOKit/pwr_mgt/IOPMLib.h>
|
||||
#elif _WIN32
|
||||
@@ -27,11 +30,13 @@
|
||||
|
||||
#include "AboutDialog.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "format.hpp"
|
||||
|
||||
#include "WebUserLoginDialog.hpp"
|
||||
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -531,6 +536,11 @@ boost::filesystem::path into_path(const wxString &str)
|
||||
return boost::filesystem::path(str.wx_str());
|
||||
}
|
||||
|
||||
wxString file_url_from_path(const boost::filesystem::path &path)
|
||||
{
|
||||
return wxFileSystem::FileNameToURL(wxFileName(from_path(path)));
|
||||
}
|
||||
|
||||
void about()
|
||||
{
|
||||
AboutDialog dlg;
|
||||
@@ -635,4 +645,26 @@ void desktop_open_any_folder( const std::string& path )
|
||||
}
|
||||
|
||||
|
||||
bool desktop_open_project_attachment(wxWindow *parent, const boost::filesystem::path &path)
|
||||
{
|
||||
// The auxiliary path is UTF-8, which is what boost::filesystem reads a narrow string as.
|
||||
const boost::filesystem::path aux_root(wxGetApp().plater()->model().get_auxiliary_file_temp_path());
|
||||
boost::system::error_code ec;
|
||||
if (!is_absolute_path_within_root(path, aux_root) || !boost::filesystem::is_regular_file(path, ec))
|
||||
return false;
|
||||
|
||||
// Attachments come with the project and carry no download mark, so the desktop would open them without a warning.
|
||||
if (!is_safe_to_open_file_name(path.filename().string())) {
|
||||
MessageDialog dlg(parent,
|
||||
wxString::Format(_L("\"%s\" is not a plain document, image or model file. Opening it may run it as a "
|
||||
"program or script on this computer.\n\n"
|
||||
"Only open attachments from projects you trust. Open it anyway?"),
|
||||
from_path(path.filename())),
|
||||
_L("Open attachment"), wxICON_WARNING | wxYES_NO);
|
||||
if (dlg.ShowModal() != wxID_YES)
|
||||
return false;
|
||||
}
|
||||
return wxLaunchDefaultApplication(from_path(path), 0);
|
||||
}
|
||||
|
||||
} }
|
||||
|
||||
@@ -76,6 +76,9 @@ std::string into_u8(const wxString &str);
|
||||
wxString from_path(const boost::filesystem::path &path);
|
||||
// boost path from wxString
|
||||
boost::filesystem::path into_path(const wxString &str);
|
||||
// file:// URL of a local path, percent-encoded so characters such as '#', '%' and '?' stay part of the path.
|
||||
// Append any query or fragment to the result.
|
||||
wxString file_url_from_path(const boost::filesystem::path &path);
|
||||
|
||||
// Display an About dialog
|
||||
extern void about();
|
||||
@@ -83,6 +86,9 @@ extern void about();
|
||||
extern void desktop_open_datadir_folder();
|
||||
// Ask the destop to open one folder
|
||||
extern void desktop_open_any_folder(const std::string& path);
|
||||
// Ask the desktop to open a file from the project's auxiliary directory, after a confirmation
|
||||
// unless its type is known to be plain content. Returns false if the file was not opened.
|
||||
extern bool desktop_open_project_attachment(wxWindow *parent, const boost::filesystem::path &path);
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
|
||||
@@ -1509,11 +1509,33 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP
|
||||
size_t n = mz_zip_reader_get_extra(&archive, stat.m_file_index, extra.data(), extra.size());
|
||||
dest_file = decode(extra.substr(0, n), stat.m_filename);
|
||||
}
|
||||
if (!is_path_within_root(dest_file, plugin_folder)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "[install_plugin] entry " << dest_file << " resolves outside " << plugin_folder.string();
|
||||
close_zip_reader(&archive);
|
||||
if (pro_fn) { pro_fn(InstallStatusUnzipFailed, 0, cancel); }
|
||||
return InstallStatusUnzipFailed;
|
||||
}
|
||||
auto dest_path = plugin_folder / dest_file;
|
||||
boost::filesystem::create_directories(dest_path.parent_path());
|
||||
std::string dest_zip_file = encode_path(dest_path.string().c_str());
|
||||
#ifndef WIN32
|
||||
// Validate a symlink's target before anything at the destination is replaced.
|
||||
const bool is_link = S_ISLNK(stat.m_external_attr >> 16);
|
||||
std::string link;
|
||||
if (is_link) {
|
||||
link.assign(stat.m_uncomp_size, 0);
|
||||
if (!mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, link.data(), stat.m_uncomp_size, 0) ||
|
||||
!is_symlink_target_within_root(dest_file, link, plugin_folder)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "[install_plugin] link " << dest_file << " -> " << link << " is unreadable or resolves outside " << plugin_folder.string();
|
||||
close_zip_reader(&archive);
|
||||
if (pro_fn) { pro_fn(InstallStatusUnzipFailed, 0, cancel); }
|
||||
return InstallStatusUnzipFailed;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
try {
|
||||
if (fs::exists(dest_path)) {
|
||||
boost::filesystem::create_directories(dest_path.parent_path());
|
||||
// symlink_status so that an existing symlink, dangling or not, is replaced rather than written through.
|
||||
if (fs::exists(fs::symlink_status(dest_path))) {
|
||||
boost::system::error_code ec;
|
||||
fs::remove(dest_path, ec);
|
||||
if (ec) {
|
||||
@@ -1541,9 +1563,8 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP
|
||||
}
|
||||
mz_bool res = 0;
|
||||
#ifndef WIN32
|
||||
if (S_ISLNK(stat.m_external_attr >> 16)) {
|
||||
std::string link(stat.m_uncomp_size + 1, 0);
|
||||
res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, link.data(), stat.m_uncomp_size, 0);
|
||||
if (is_link) {
|
||||
res = 1;
|
||||
try {
|
||||
boost::filesystem::create_symlink(link, dest_path);
|
||||
} catch (const std::exception &e) {
|
||||
|
||||
@@ -336,13 +336,20 @@ wxBitmap SettingsFactory::get_category_bitmap(const std::string& category_name,
|
||||
//-------------------------------------
|
||||
|
||||
// Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important
|
||||
static const constexpr std::array<std::pair<const char *, const char *>, 5> ADD_VOLUME_MENU_ITEMS = {{
|
||||
static const constexpr std::array<std::pair<const char *, const char *>, 11> ADD_VOLUME_MENU_ITEMS = {{
|
||||
// menu_item Name menu_item bitmap name
|
||||
{L("Add Part"), "menu_add_part" }, // ~ModelVolumeType::MODEL_PART
|
||||
{L("Add Negative Part"), "menu_add_negative" }, // ~ModelVolumeType::NEGATIVE_VOLUME
|
||||
{L("Add Modifier"), "menu_add_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER
|
||||
{L("Add Support Blocker"), "menu_support_blocker"}, // ~ModelVolumeType::SUPPORT_BLOCKER
|
||||
{L("Add Support Enforcer"), "menu_support_enforcer"}, // ~ModelVolumeType::SUPPORT_ENFORCER
|
||||
// Precise Seam modifiers (all 6 subtypes - only first one shown in menu, others used for tree icons)
|
||||
{L("Add Precise Seam"), "menu_precise_seam_center"}, // ~ModelVolumeType::PRECISE_SEAM_CENTER
|
||||
{L("Add Precise Seam"), "menu_precise_seam_left"}, // ~ModelVolumeType::PRECISE_SEAM_LEFT
|
||||
{L("Add Precise Seam"), "menu_precise_seam_right"}, // ~ModelVolumeType::PRECISE_SEAM_RIGHT
|
||||
{L("Add Precise Seam"), "menu_precise_seam_enforced"}, // ~ModelVolumeType::PRECISE_SEAM_ENFORCED
|
||||
{L("Add Precise Seam"), "menu_precise_seam_blocked"}, // ~ModelVolumeType::PRECISE_SEAM_BLOCKED
|
||||
{L("Add Precise Seam"), "menu_precise_seam_neutral"}, // ~ModelVolumeType::PRECISE_SEAM_NEUTRAL
|
||||
}};
|
||||
|
||||
// Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important
|
||||
@@ -740,10 +747,20 @@ void MenuFactory::append_menu_items_add_volume(wxMenu* menu)
|
||||
|
||||
for (size_t type = 0; type < ADD_VOLUME_MENU_ITEMS.size(); type++)
|
||||
{
|
||||
// Skip Precise Seam subtypes except the first one (CENTER) - they are only used for tree icons
|
||||
if (type >= int(ModelVolumeType::PRECISE_SEAM_LEFT) &&
|
||||
type <= int(ModelVolumeType::PRECISE_SEAM_NEUTRAL))
|
||||
continue;
|
||||
|
||||
auto& item = ADD_VOLUME_MENU_ITEMS[type];
|
||||
|
||||
// Use special icon for "Add Precise Seam" menu (different from tree icon)
|
||||
std::string icon_name = item.second;
|
||||
if (type == int(ModelVolumeType::PRECISE_SEAM_CENTER))
|
||||
icon_name = "menu_precise_seam_add";
|
||||
|
||||
wxMenu* sub_menu = append_submenu_add_generic(menu, ModelVolumeType(type));
|
||||
append_submenu(menu, sub_menu, wxID_ANY, _(item.first), "", item.second,
|
||||
append_submenu(menu, sub_menu, wxID_ANY, _(item.first), "", icon_name,
|
||||
[]() { return obj_list()->is_instance_or_object_selected(); }, m_parent);
|
||||
}
|
||||
|
||||
@@ -843,11 +860,16 @@ wxMenuItem* MenuFactory::append_menu_item_change_type(wxMenu* menu)
|
||||
};
|
||||
|
||||
std::vector<TypeInfo> types = {
|
||||
{ ModelVolumeType::MODEL_PART, _L("Part") },
|
||||
{ ModelVolumeType::NEGATIVE_VOLUME, _L("Negative Part") },
|
||||
{ ModelVolumeType::PARAMETER_MODIFIER, _L("Modifier") },
|
||||
{ ModelVolumeType::SUPPORT_BLOCKER, _L("Support Blocker") },
|
||||
{ ModelVolumeType::SUPPORT_ENFORCER, _L("Support Enforcer") }
|
||||
{ ModelVolumeType::MODEL_PART, _L("Part") },
|
||||
{ ModelVolumeType::NEGATIVE_VOLUME, _L("Negative Part") },
|
||||
{ ModelVolumeType::PARAMETER_MODIFIER, _L("Modifier") },
|
||||
{ ModelVolumeType::SUPPORT_BLOCKER, _L("Support Blocker") },
|
||||
{ ModelVolumeType::SUPPORT_ENFORCER, _L("Support Enforcer") },
|
||||
// Single "Precise Seam" entry that maps to PRECISE_SEAM_CENTER as the default subtype.
|
||||
// set_volume_type() preserves the existing subtype (LEFT/RIGHT/etc.) for volumes that
|
||||
// are already Precise Seam; subtype picking is done via the separate
|
||||
// "Precise Seam Type" submenu (see append_menu_item_precise_seam_submenu).
|
||||
{ ModelVolumeType::PRECISE_SEAM_CENTER, _L("Precise Seam") }
|
||||
};
|
||||
|
||||
for (const auto& info : types) {
|
||||
@@ -863,7 +885,13 @@ wxMenuItem* MenuFactory::append_menu_item_change_type(wxMenu* menu)
|
||||
obj_list()->GetSelections(sels);
|
||||
for (auto item : sels) {
|
||||
ModelVolumeType vol_type = model->GetVolumeType(item);
|
||||
if (vol_type == type) {
|
||||
// The "Precise Seam" entry represents all six PS subtypes, so any subtype
|
||||
// among selected volumes counts as a match (keeps the checkbox ticked when
|
||||
// user has PS_LEFT/RIGHT/etc. selected, not only plain CENTER).
|
||||
const bool match = (type == ModelVolumeType::PRECISE_SEAM_CENTER)
|
||||
? is_precise_seam(vol_type)
|
||||
: (vol_type == type);
|
||||
if (match) {
|
||||
has_type = true;
|
||||
break;
|
||||
}
|
||||
@@ -871,6 +899,7 @@ wxMenuItem* MenuFactory::append_menu_item_change_type(wxMenu* menu)
|
||||
evt.Check(has_type);
|
||||
|
||||
// ORCA Fix crash caused by SVG/TEXT volumes cant be Support Enforcer/Blocker type
|
||||
// The same applies to Precise Seam subtypes.
|
||||
for (auto item : sels) {
|
||||
if (model->GetItemType(item) == itVolume){
|
||||
auto vol_idx = model->GetVolumeIdByItem(item);
|
||||
@@ -881,7 +910,8 @@ wxMenuItem* MenuFactory::append_menu_item_change_type(wxMenu* menu)
|
||||
auto vol = (*objs)[obj_idx]->volumes[vol_idx];
|
||||
|
||||
// disable Support Enforcer/Blocker if selection contains svg or text
|
||||
if (vol != nullptr && (vol->is_svg() || vol->is_text()) && (type == ModelVolumeType::SUPPORT_BLOCKER || type == ModelVolumeType::SUPPORT_ENFORCER)){
|
||||
// (same applies to Precise Seam)
|
||||
if (vol != nullptr && (vol->is_svg() || vol->is_text()) && (type == ModelVolumeType::SUPPORT_BLOCKER || type == ModelVolumeType::SUPPORT_ENFORCER || is_precise_seam(type))){
|
||||
evt.Enable(false);
|
||||
break;
|
||||
}
|
||||
@@ -1309,6 +1339,131 @@ void MenuFactory::append_menu_items_mirror(wxMenu* menu)
|
||||
[]() { return plater()->can_mirror(); }, m_parent);
|
||||
}
|
||||
|
||||
void MenuFactory::append_menu_item_precise_seam_submenu(wxMenu* menu)
|
||||
{
|
||||
wxString submenu_name = _L("Precise Seam Type");
|
||||
|
||||
// Remove existing submenu if present (menu is rebuilt on every right-click)
|
||||
const int menu_item_id = menu->FindItem(submenu_name);
|
||||
if (menu_item_id != wxNOT_FOUND)
|
||||
menu->Destroy(menu_item_id);
|
||||
|
||||
// --- Precondition: ALL selected volumes must be Precise Seam ---
|
||||
// Mixed selections (PS + non-PS) are ambiguous for subtype switching: applying a subtype
|
||||
// would implicitly convert the non-PS volumes to PS, which is not what the user expects
|
||||
// from a subtype picker. For mixed selections the user should first use
|
||||
// "Change type → Precise Seam" to unify them, then come back to this submenu.
|
||||
wxDataViewItemArray sels;
|
||||
obj_list()->GetSelections(sels);
|
||||
if (sels.IsEmpty())
|
||||
return;
|
||||
|
||||
// A right-click on a volume in the 3D view routes through Plater::show_context_menu
|
||||
// → ObjectList::update_selections(), which intentionally preserves a pre-existing
|
||||
// settings-row selection alongside the clicked volume. As a result GetSelections()
|
||||
// may return settings-row items that are children of a volume. Treat each settings
|
||||
// row as selecting its parent volume (same pattern used by set_volume_type()
|
||||
// internally). Without this resolution, GetVolumeType(settings_row) returns INVALID
|
||||
// and the submenu would hide even when a valid PS volume is in the selection.
|
||||
//
|
||||
// Precomputing the resolved PS types once here also lets the checkmark loop below
|
||||
// display checks correctly for selections that contain settings rows.
|
||||
std::vector<ModelVolumeType> selected_ps_types;
|
||||
selected_ps_types.reserve(sels.size());
|
||||
|
||||
auto* model = obj_list()->GetModel();
|
||||
for (const auto& sel_item : sels) {
|
||||
wxDataViewItem vol_item = sel_item;
|
||||
const ItemType type_mask = model->GetItemType(sel_item);
|
||||
if (!(type_mask & itVolume)) {
|
||||
// Only settings rows whose parent is a volume map to "selecting that volume".
|
||||
// Any other non-volume item (object, instance, etc.) means the selection is
|
||||
// not purely PS-volume-based — hide the submenu.
|
||||
if ((type_mask & itSettings) && (model->GetItemType(model->GetParent(sel_item)) & itVolume))
|
||||
vol_item = model->GetParent(sel_item);
|
||||
else
|
||||
return;
|
||||
}
|
||||
const ModelVolumeType vol_type = model->GetVolumeType(vol_item);
|
||||
if (!is_precise_seam(vol_type))
|
||||
return;
|
||||
selected_ps_types.push_back(vol_type);
|
||||
}
|
||||
|
||||
// Match the label used by append_menu_item_change_type, including its translation.
|
||||
int insert_pos = wxNOT_FOUND;
|
||||
int change_type_id = menu->FindItem(_L("Change Type"));
|
||||
if (change_type_id != wxNOT_FOUND) {
|
||||
for (size_t i = 0; i < menu->GetMenuItemCount(); i++) {
|
||||
wxMenuItem* item = menu->FindItemByPosition(i);
|
||||
if (item && item->GetId() == change_type_id) {
|
||||
insert_pos = i + 1; // insert directly after the Change Type entry
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Build the subtype submenu ---
|
||||
wxMenu* ps_menu = new wxMenu();
|
||||
|
||||
// Array of all 6 Precise Seam subtypes with labels.
|
||||
// "Seam ..." prefix disambiguates from other i18n contexts (extruder Left/Right, "Center on bed", etc.)
|
||||
static const std::array<std::pair<const char*, ModelVolumeType>, 6> PS_TYPES = {{
|
||||
{L("Seam Center"), ModelVolumeType::PRECISE_SEAM_CENTER},
|
||||
{L("Seam Left"), ModelVolumeType::PRECISE_SEAM_LEFT},
|
||||
{L("Seam Right"), ModelVolumeType::PRECISE_SEAM_RIGHT},
|
||||
{L("Seam Enforced"), ModelVolumeType::PRECISE_SEAM_ENFORCED},
|
||||
{L("Seam Blocked"), ModelVolumeType::PRECISE_SEAM_BLOCKED},
|
||||
{L("Seam Neutral"), ModelVolumeType::PRECISE_SEAM_NEUTRAL},
|
||||
}};
|
||||
|
||||
for (const auto& ps_type : PS_TYPES) {
|
||||
wxString label = _(ps_type.first);
|
||||
|
||||
// Check-items (not radio): radio groups in wxWidgets auto-select the first item when
|
||||
// no item is explicitly checked, which misleads the user into seeing "Seam Center"
|
||||
// as the active subtype for mixed-subtype selections. With check-items we can leave
|
||||
// every item unchecked in that case, and the menu matches the pattern of the
|
||||
// neighbouring "Change type" submenu (see append_menu_item_change_type).
|
||||
//
|
||||
// Handler: delegate to set_volume_type() with preserve_ps_subtype=false. The user
|
||||
// explicitly picked a subtype here, so "Seam Center" must set every selected PS
|
||||
// volume to CENTER verbatim — preservation (the default for "Change type") would
|
||||
// keep existing subtypes and make CENTER unreachable on mixed-subtype selections.
|
||||
// set_volume_type() already handles multi-select iteration, the last-solid-part
|
||||
// guard, and the PS group-change reordering via move_volume_to_end().
|
||||
wxMenuItem* item = append_menu_check_item(ps_menu, wxID_ANY, label, "",
|
||||
[ps_type](wxCommandEvent&) {
|
||||
obj_list()->set_volume_type(ps_type.second, /*preserve_ps_subtype=*/false);
|
||||
},
|
||||
ps_menu);
|
||||
|
||||
// Tick every subtype that is present in the current selection. Same pattern as the
|
||||
// neighbouring "Change type" submenu (where selecting [Part + Modifier] ticks both
|
||||
// "Part" and "Modifier" boxes). For homogeneous selections exactly one checkbox is
|
||||
// ticked; for mixed subtype selections (e.g. PS_LEFT + PS_RIGHT) both "Seam Left"
|
||||
// and "Seam Right" appear checked, so the user sees at a glance which subtypes are
|
||||
// currently in the selection.
|
||||
//
|
||||
// Uses the precomputed selected_ps_types (which resolved settings rows to their
|
||||
// parent volumes); iterating sels directly would miss subtypes for selections that
|
||||
// arrive via settings rows from right-click-in-3D-view.
|
||||
const bool is_present = std::find(selected_ps_types.begin(), selected_ps_types.end(),
|
||||
ps_type.second) != selected_ps_types.end();
|
||||
if (is_present && item)
|
||||
item->Check(true);
|
||||
}
|
||||
|
||||
// Add submenu to parent menu
|
||||
append_submenu(menu, ps_menu, wxID_ANY,
|
||||
submenu_name,
|
||||
_L("Choose precise seam subtype"),
|
||||
"menu_precise_seam_type",
|
||||
[]() { return true; },
|
||||
m_parent,
|
||||
insert_pos);
|
||||
}
|
||||
|
||||
void MenuFactory::append_menu_item_edit_text(wxMenu *menu)
|
||||
{
|
||||
wxString name = _L("Edit text");
|
||||
@@ -1578,6 +1733,7 @@ void MenuFactory::create_part_menu()
|
||||
|
||||
menu->AppendSeparator();
|
||||
append_menu_item_change_type(menu);
|
||||
append_menu_item_precise_seam_submenu(menu);
|
||||
append_menu_items_mirror(&m_part_menu);
|
||||
append_menu_item(&m_part_menu, wxID_ANY, _L("Split"), _L("Split the selected object into multiple parts"),
|
||||
[](wxCommandEvent&) { plater()->split_volume(); }, "split_parts", nullptr,
|
||||
@@ -1875,6 +2031,7 @@ wxMenu* MenuFactory::part_menu()
|
||||
{
|
||||
append_menu_items_convert_unit(&m_part_menu);
|
||||
append_menu_item_change_filament(&m_part_menu);
|
||||
append_menu_item_precise_seam_submenu(&m_part_menu);
|
||||
append_menu_item_per_object_settings(&m_part_menu);
|
||||
return &m_part_menu;
|
||||
}
|
||||
@@ -1989,6 +2146,10 @@ wxMenu* MenuFactory::multi_selection_menu()
|
||||
append_menu_item_per_object_process(menu);
|
||||
menu->AppendSeparator();
|
||||
append_menu_item_change_type(menu);
|
||||
// Subtype picker for Precise Seam — shown only when all selected volumes are PS.
|
||||
// Must be paired with Change Type here (as in single-volume part_menu), otherwise
|
||||
// the user cannot switch PS subtypes (LEFT/RIGHT/etc.) on multi-selection.
|
||||
append_menu_item_precise_seam_submenu(menu);
|
||||
append_menu_item_change_filament(menu);
|
||||
}
|
||||
return menu;
|
||||
|
||||
@@ -174,6 +174,7 @@ private:
|
||||
void append_menu_item_merge_to_single_object(wxMenu* menu);
|
||||
void append_menu_item_merge_parts_to_single_part(wxMenu *menu);
|
||||
void append_menu_items_mirror(wxMenu *menu);
|
||||
void append_menu_item_precise_seam_submenu(wxMenu* menu);
|
||||
void append_menu_item_invalidate_cut_info(wxMenu *menu);
|
||||
void append_menu_item_edit_text(wxMenu *menu);
|
||||
void append_menu_item_edit_svg(wxMenu *menu);
|
||||
|
||||
@@ -1944,6 +1944,12 @@ bool ObjectList::can_drop(const wxDataViewItem& item, int& src_obj_id, int& src_
|
||||
|
||||
if (dragged_item_v_type == item_v_type && dragged_item_v_type != ModelVolumeType::MODEL_PART)
|
||||
return true;
|
||||
|
||||
// Use tree item types: hidden cut connectors make tree indices differ from volumes indices.
|
||||
if (is_precise_seam(dragged_item_v_type) && is_precise_seam(item_v_type))
|
||||
// Allow reordering only within the strong or weak modifier group.
|
||||
return is_precise_seam_strong(dragged_item_v_type) == is_precise_seam_strong(item_v_type);
|
||||
|
||||
if ((dragged_item_v_type != item_v_type) || // we can't reorder volumes outside of types
|
||||
item_v_type >= ModelVolumeType::SUPPORT_BLOCKER) // support blockers/enforcers can't change its place
|
||||
return false;
|
||||
@@ -2033,10 +2039,43 @@ void ObjectList::OnDrop(wxDataViewEvent &event)
|
||||
int to_volume_id = m_objects_model->GetVolumeIdByItem(item);
|
||||
int delta = to_volume_id < from_volume_id ? -1 : 1;
|
||||
|
||||
auto &volumes = (*m_objects)[m_dragged_data.obj_idx()]->volumes;
|
||||
const int obj_idx = m_dragged_data.obj_idx();
|
||||
// Object-indexed UI maps may be stale after another object is removed or reordered.
|
||||
if (obj_idx < 0 || size_t(obj_idx) >= m_objects->size()) {
|
||||
event.Veto();
|
||||
m_dragged_data.clear();
|
||||
return;
|
||||
}
|
||||
const ModelObject *object = (*m_objects)[obj_idx];
|
||||
auto &volumes = (*m_objects)[obj_idx]->volumes;
|
||||
std::vector<size_t> visible_volume_indices;
|
||||
visible_volume_indices.reserve(volumes.size());
|
||||
for (size_t idx = 0; idx < volumes.size(); ++idx)
|
||||
// Match add_volumes_to_object_in_list: only connectors of cut objects are hidden.
|
||||
if (!(object->is_cut() && volumes[idx]->is_cut_connector()))
|
||||
visible_volume_indices.push_back(idx);
|
||||
|
||||
int cnt = 0;
|
||||
for (int id = from_volume_id; cnt < abs(from_volume_id - to_volume_id); id += delta, cnt++) std::swap(volumes[id], volumes[id + delta]);
|
||||
// Validate the entire move before any swap; these checks must also protect Release builds.
|
||||
if (from_volume_id < 0 || to_volume_id < 0 ||
|
||||
size_t(from_volume_id) >= visible_volume_indices.size() || size_t(to_volume_id) >= visible_volume_indices.size()) {
|
||||
event.Veto();
|
||||
m_dragged_data.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move through visible slots only, keeping hidden cut connectors at their original indices.
|
||||
// The local mapping stays valid because no hidden volume changes slots during the move.
|
||||
for (int id = from_volume_id; id != to_volume_id; id += delta) {
|
||||
const size_t current_idx = visible_volume_indices[id];
|
||||
const size_t next_idx = visible_volume_indices[id + delta];
|
||||
std::swap(volumes[current_idx], volumes[next_idx]);
|
||||
}
|
||||
|
||||
// Later selection/filament handlers use this cache; repair it for the moved object too.
|
||||
auto &ui_to_model = m_objects_model->get_ui_and_3d_volume_map()[obj_idx];
|
||||
ui_to_model.clear();
|
||||
for (size_t idx = 0; idx < visible_volume_indices.size(); ++idx)
|
||||
ui_to_model[int(idx)] = int(visible_volume_indices[idx]);
|
||||
|
||||
select_item(m_objects_model->ReorganizeChildren(from_volume_id, to_volume_id, m_objects_model->GetParent(item)));
|
||||
|
||||
@@ -3855,7 +3894,8 @@ wxDataViewItem ObjectList::add_settings_item(wxDataViewItem parent_item, const D
|
||||
const bool is_layer_settings = m_objects_model->GetItemType(parent_item) == itLayer;
|
||||
if (!is_object_settings) {
|
||||
ModelVolumeType volume_type = m_objects_model->GetVolumeType(parent_item);
|
||||
if (volume_type == ModelVolumeType::NEGATIVE_VOLUME || volume_type == ModelVolumeType::SUPPORT_BLOCKER || volume_type == ModelVolumeType::SUPPORT_ENFORCER)
|
||||
// Precise Seam is non-printing helper geometry — no per-volume settings
|
||||
if (volume_type == ModelVolumeType::NEGATIVE_VOLUME || volume_type == ModelVolumeType::SUPPORT_BLOCKER || volume_type == ModelVolumeType::SUPPORT_ENFORCER || is_precise_seam(volume_type))
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -5712,7 +5752,6 @@ void ObjectList::change_part_type()
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
ModelVolumeType ObjectList::get_selected_volume_type()
|
||||
{
|
||||
ModelVolume* volume = get_selected_model_volume();
|
||||
@@ -5721,13 +5760,49 @@ ModelVolumeType ObjectList::get_selected_volume_type()
|
||||
return ModelVolumeType::INVALID;
|
||||
}
|
||||
|
||||
void ObjectList::set_volume_type(ModelVolumeType new_type)
|
||||
// ---------------- Helpers for Precise Seam group-aware type changes ----------------
|
||||
// Used by set_volume_type() and the "Precise Seam Type" subtype submenu handler.
|
||||
|
||||
// Detects whether a type change crosses a Precise Seam "group boundary" that requires
|
||||
// manual repositioning inside ModelObject::volumes[]:
|
||||
// - between any non-PS type and any PS subtype, or
|
||||
// - between strong PS (CENTER/LEFT/RIGHT) and weak PS (ENFORCED/BLOCKED/NEUTRAL).
|
||||
// sort_volumes() is stable and groups strong PS before weak PS. When a volume crosses
|
||||
// a group, moving it to the end of volumes[] lets the subsequent stable sort place it
|
||||
// at the end of its new group. Without this, a strong→weak transition would leave the
|
||||
// volume stuck in the strong segment and break the modifier-application order.
|
||||
static bool precise_seam_group_changed(ModelVolumeType old_type, ModelVolumeType new_type)
|
||||
{
|
||||
const bool old_is_ps = is_precise_seam(old_type);
|
||||
const bool new_is_ps = is_precise_seam(new_type);
|
||||
if (old_is_ps != new_is_ps)
|
||||
return true; // non-PS ↔ PS transition
|
||||
if (!old_is_ps)
|
||||
return false; // both non-PS — regular enum ordering is enough
|
||||
return is_precise_seam_strong(old_type) != is_precise_seam_strong(new_type);
|
||||
}
|
||||
|
||||
// Moves a volume to the end of ModelObject::volumes[]. Combined with stable sort_volumes(),
|
||||
// this places it at the end of its new group while preserving relative order of others.
|
||||
static void move_volume_to_end(ModelObject* obj, ModelVolume* volume)
|
||||
{
|
||||
if (obj == nullptr || volume == nullptr)
|
||||
return;
|
||||
auto it = std::find(obj->volumes.begin(), obj->volumes.end(), volume);
|
||||
if (it != obj->volumes.end()) {
|
||||
obj->volumes.erase(it);
|
||||
obj->volumes.push_back(volume);
|
||||
}
|
||||
}
|
||||
|
||||
void ObjectList::set_volume_type(ModelVolumeType new_type, bool preserve_ps_subtype)
|
||||
{
|
||||
struct VolumeSelection {
|
||||
int object_idx;
|
||||
ModelVolume* volume;
|
||||
};
|
||||
|
||||
// --- Collect selected volumes from the object tree, falling back to the 3D canvas ---
|
||||
std::vector<VolumeSelection> volumes;
|
||||
auto add_volume = [&volumes](int obj_idx, ModelVolume* volume) {
|
||||
if (volume == nullptr)
|
||||
@@ -5792,25 +5867,49 @@ void ObjectList::set_volume_type(ModelVolumeType new_type)
|
||||
// and historically crashed (originally fixed in the now-disabled change_part_type() by hiding
|
||||
// Support entries in the old choice dialog; the UI-side guard for the current submenu lives
|
||||
// in MenuFactory::append_menu_item_change_type, see #13120).
|
||||
// The same rationale applies to any Precise Seam subtype.
|
||||
// This block must never be reachable under a healthy UI; if it ever logs, the UI guard has
|
||||
// been bypassed (new entry point, refactor, plugin, etc.) and should be investigated.
|
||||
if (new_type == ModelVolumeType::SUPPORT_BLOCKER || new_type == ModelVolumeType::SUPPORT_ENFORCER) {
|
||||
if (new_type == ModelVolumeType::SUPPORT_BLOCKER || new_type == ModelVolumeType::SUPPORT_ENFORCER
|
||||
|| is_precise_seam(new_type)) {
|
||||
const bool has_text_or_svg = std::any_of(volumes.begin(), volumes.end(),
|
||||
[](const VolumeSelection& sel) { return sel.volume->is_svg() || sel.volume->is_text(); });
|
||||
if (has_text_or_svg) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__
|
||||
<< ": blocked attempt to set SUPPORT_BLOCKER/ENFORCER on SVG/text volume; "
|
||||
<< ": blocked attempt to set SUPPORT_BLOCKER/ENFORCER or Precise Seam on SVG/text volume; "
|
||||
<< "UI guard should have prevented this -- possible regression in the Change Type menu";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Subtype preservation for the generic "Precise Seam" entry ---
|
||||
// The Change Type submenu uses PRECISE_SEAM_CENTER as the single "Precise Seam" entry
|
||||
// and calls with preserve_ps_subtype=true: volumes that are already Precise Seam keep
|
||||
// their subtype (LEFT/RIGHT/etc.); only non-PS volumes get converted to the default
|
||||
// CENTER subtype.
|
||||
//
|
||||
// The "Precise Seam Type" subtype submenu calls with preserve_ps_subtype=false: the
|
||||
// user explicitly picked CENTER and all selected PS volumes must be set to CENTER
|
||||
// verbatim (otherwise CENTER would become unreachable for mixed-subtype selections,
|
||||
// since preservation would keep every volume at its current subtype and any_diff would
|
||||
// be false — the reason for this two-parameter split).
|
||||
auto effective_new_type = [new_type, preserve_ps_subtype](const ModelVolume* v) -> ModelVolumeType {
|
||||
if (preserve_ps_subtype && new_type == ModelVolumeType::PRECISE_SEAM_CENTER && v->is_precise_seam())
|
||||
return v->type();
|
||||
return new_type;
|
||||
};
|
||||
|
||||
// --- Any-change check using the effective target type ---
|
||||
const bool any_diff = std::any_of(volumes.begin(), volumes.end(),
|
||||
[new_type](const VolumeSelection& sel) { return sel.volume->type() != new_type; });
|
||||
[&effective_new_type](const VolumeSelection& sel) {
|
||||
return sel.volume->type() != effective_new_type(sel.volume);
|
||||
});
|
||||
|
||||
if (!any_diff)
|
||||
return;
|
||||
|
||||
// --- Last-solid-part guard (pre-existing behavior) ---
|
||||
// When converting away from MODEL_PART, ensure at least one solid part remains per object.
|
||||
if (new_type != ModelVolumeType::MODEL_PART) {
|
||||
std::map<int, int> total_part_cnt;
|
||||
std::map<int, int> selected_part_cnt;
|
||||
@@ -5838,14 +5937,36 @@ void ObjectList::set_volume_type(ModelVolumeType new_type)
|
||||
|
||||
take_snapshot(_u8L("Change part type"));
|
||||
|
||||
// --- Apply type changes with Precise Seam group-aware repositioning ---
|
||||
// Note: `changed_volumes` / `touched_objects` track every processed volume, including
|
||||
// subtype-preservation no-ops. Tracking no-ops is intentional — it ensures such
|
||||
// volumes remain selected after the post-apply rebuild. Without that, a cross-object
|
||||
// mixed selection like [Part, PS_LEFT] clicking "Change type → Precise Seam" would
|
||||
// silently deselect the preserved PS_LEFT: only the Part's object would enter
|
||||
// touched_objects, and the final SetSelections(new_selection) would drop PS_LEFT.
|
||||
std::set<const ModelVolume*> changed_volumes;
|
||||
std::set<int> touched_objects;
|
||||
for (const auto& sel : volumes) {
|
||||
sel.volume->set_type(new_type);
|
||||
const ModelVolumeType target = effective_new_type(sel.volume);
|
||||
const ModelVolumeType old_type = sel.volume->type();
|
||||
|
||||
// Record the volume for reselection before the no-op short-circuit (see comment above).
|
||||
changed_volumes.insert(sel.volume);
|
||||
touched_objects.insert(sel.object_idx);
|
||||
|
||||
if (old_type == target)
|
||||
continue; // subtype-preservation no-op — nothing to write/move
|
||||
|
||||
sel.volume->set_type(target);
|
||||
|
||||
// If the change crosses a PS group boundary, move the volume to the end of volumes[]
|
||||
// so the stable sort in reorder_volumes_and_get_selection() places it at the end of
|
||||
// its new group (see precise_seam_group_changed() for details).
|
||||
if (precise_seam_group_changed(old_type, target))
|
||||
move_volume_to_end((*m_objects)[sel.object_idx], sel.volume);
|
||||
}
|
||||
|
||||
// --- Rebuild selection to follow the changed volumes ---
|
||||
wxDataViewItemArray new_selection;
|
||||
for (int obj_idx : touched_objects) {
|
||||
wxDataViewItemArray sel_items = reorder_volumes_and_get_selection(obj_idx, [&changed_volumes](const ModelVolume* volume) {
|
||||
|
||||
@@ -88,6 +88,7 @@ struct MeshErrorsInfo
|
||||
class ObjectList : public wxDataViewCtrl
|
||||
{
|
||||
public:
|
||||
|
||||
enum SELECTION_MODE
|
||||
{
|
||||
smUndef = 0,
|
||||
@@ -430,7 +431,12 @@ public:
|
||||
#if 0 // ORCA: disabled alongside definition in GUI_ObjectList.cpp (see #if 0 block there)
|
||||
void change_part_type();
|
||||
#endif
|
||||
void set_volume_type(ModelVolumeType new_type);
|
||||
// preserve_ps_subtype = true: when new_type is PRECISE_SEAM_CENTER, volumes that are
|
||||
// already Precise Seam keep their existing subtype (LEFT/RIGHT/etc.). Used by the
|
||||
// generic "Change type → Precise Seam" entry where CENTER is a default fallback.
|
||||
// preserve_ps_subtype = false: no preservation — target type is applied verbatim. Used
|
||||
// by the "Precise Seam Type" subtype picker where the user explicitly wants CENTER.
|
||||
void set_volume_type(ModelVolumeType new_type, bool preserve_ps_subtype = true);
|
||||
ModelVolumeType get_selected_volume_type();
|
||||
|
||||
void last_volume_is_deleted(const int obj_idx);
|
||||
|
||||
@@ -348,7 +348,8 @@ void GridCellFilamentsRenderer::Draw(wxGrid &grid, wxGridCellAttr &attr, wxDC &d
|
||||
if ((grid_row->model_volume_type != ModelVolumeType::NEGATIVE_VOLUME) && \
|
||||
(grid_row->model_volume_type != ModelVolumeType::SUPPORT_BLOCKER) && \
|
||||
(grid_row->model_volume_type != ModelVolumeType::SUPPORT_ENFORCER) && \
|
||||
(grid_row->model_volume_type != ModelVolumeType::PARAMETER_MODIFIER)) {
|
||||
(grid_row->model_volume_type != ModelVolumeType::PARAMETER_MODIFIER) && \
|
||||
!is_precise_seam(grid_row->model_volume_type)) { // Precise Seam is non-printing helper geometry
|
||||
dc.DrawBitmap(*bitmap, wxPoint(rect.x + offset_x, rect.y + offset_y));
|
||||
}
|
||||
else if (grid_row->model_volume_type == ModelVolumeType::PARAMETER_MODIFIER){
|
||||
@@ -3011,7 +3012,8 @@ void ObjectTablePanel::load_data()
|
||||
if (col == ObjectGridTable::col_filaments) {
|
||||
if ((grid_row->model_volume_type != ModelVolumeType::NEGATIVE_VOLUME) && \
|
||||
(grid_row->model_volume_type != ModelVolumeType::SUPPORT_BLOCKER) && \
|
||||
(grid_row->model_volume_type != ModelVolumeType::SUPPORT_ENFORCER)) {
|
||||
(grid_row->model_volume_type != ModelVolumeType::SUPPORT_ENFORCER) && \
|
||||
!is_precise_seam(grid_row->model_volume_type)) { // Precise Seam is non-printing helper geometry
|
||||
GridCellFilamentsEditor* filament_editor = new GridCellFilamentsEditor(grid_col->choices, false, &m_color_bitmaps);
|
||||
m_object_grid->SetCellEditor(row, col, filament_editor);
|
||||
m_object_grid->SetCellRenderer(row, col, new GridCellFilamentsRenderer());
|
||||
|
||||
@@ -2000,7 +2000,7 @@ void GLGizmoEmboss::draw_model_type()
|
||||
if ((is_volume_move_inside || is_volume_move_outside))
|
||||
process();
|
||||
|
||||
// inspiration in ObjectList::change_part_type()
|
||||
// inspiration in ObjectList::set_volume_type()
|
||||
// how to view correct side panel with objects
|
||||
ObjectList *obj_list = app.obj_list();
|
||||
wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection(
|
||||
|
||||
@@ -2052,7 +2052,7 @@ void GLGizmoSVG::draw_model_type()
|
||||
if ((is_volume_move_inside || is_volume_move_outside))
|
||||
process();
|
||||
|
||||
// inspiration in ObjectList::change_part_type()
|
||||
// inspiration in ObjectList::set_volume_type()
|
||||
// how to view correct side panel with objects
|
||||
ObjectList *obj_list = app.obj_list();
|
||||
wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection(
|
||||
|
||||
+150
-216
@@ -1997,6 +1997,126 @@ bool MainFrame::can_reslice() const
|
||||
return (m_plater != nullptr) && !m_plater->model().objects.empty();
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Orca: stable key persisted by the "remember last print action" preference. Reordering PrintSelectType
|
||||
// must not silently remap a saved preference, so the key never derives from the enum value.
|
||||
const char* print_select_type_key(MainFrame::PrintSelectType type)
|
||||
{
|
||||
switch (type) {
|
||||
case MainFrame::ePrintAll: return "print_all";
|
||||
case MainFrame::ePrintPlate: return "print_plate";
|
||||
case MainFrame::eExportSlicedFile: return "export_sliced_file";
|
||||
case MainFrame::eExportAllSlicedFile: return "export_all_sliced_file";
|
||||
case MainFrame::eExportGcode: return "export_gcode";
|
||||
case MainFrame::eSendGcode: return "send_gcode";
|
||||
case MainFrame::eSendToPrinter: return "send_to_printer";
|
||||
case MainFrame::eSendToPrinterAll: return "send_to_printer_all";
|
||||
case MainFrame::ePrintMultiMachine: return "print_multi_machine";
|
||||
case MainFrame::eUploadGcode: break; // Orca: no dropdown entry, never selectable
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Orca: single source for the print button and print dropdown labels
|
||||
wxString print_select_type_label(MainFrame::PrintSelectType type)
|
||||
{
|
||||
switch (type) {
|
||||
case MainFrame::ePrintAll: return _L("Print all");
|
||||
case MainFrame::ePrintPlate: return _L("Print plate");
|
||||
case MainFrame::eExportSlicedFile: return _L("Export plate sliced file");
|
||||
case MainFrame::eExportAllSlicedFile: return _L("Export all sliced file");
|
||||
case MainFrame::eExportGcode: return _L("Export G-code file");
|
||||
case MainFrame::eSendGcode: return _L_CONTEXT("Print", "Verb");
|
||||
case MainFrame::eSendToPrinter: return _L("Send");
|
||||
case MainFrame::eSendToPrinterAll: return _L("Send all");
|
||||
case MainFrame::ePrintMultiMachine: return _L("Send to Multi-device");
|
||||
case MainFrame::eUploadGcode: break; // Orca: no dropdown entry, never selectable
|
||||
}
|
||||
return _L("Print plate");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<MainFrame::PrintSelectType> MainFrame::available_print_actions() const
|
||||
{
|
||||
std::vector<PrintSelectType> actions;
|
||||
const auto preset_bundle = wxGetApp().preset_bundle;
|
||||
const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents");
|
||||
|
||||
if (preset_bundle && !preset_bundle->is_bbl_vendor() && !use_printer_agents) {
|
||||
// ThirdParty actions
|
||||
actions.push_back(eSendGcode);
|
||||
// Orca: when the printer accepts a .gcode.3mf (the "Support 3MF as gcode" option),
|
||||
// also offer exporting the sliced .gcode.3mf bundle
|
||||
const auto* use_3mf_opt = preset_bundle->printers.get_edited_preset().config.option<ConfigOptionBool>("use_3mf");
|
||||
if (use_3mf_opt != nullptr && use_3mf_opt->value)
|
||||
actions.push_back(eExportSlicedFile);
|
||||
actions.push_back(eExportGcode);
|
||||
return actions;
|
||||
}
|
||||
|
||||
// Orca Slicer actions
|
||||
bool support_send = true;
|
||||
bool support_print_all = true;
|
||||
if (preset_bundle && !preset_bundle->use_bbl_network() && !use_printer_agents) {
|
||||
support_send = false; // All 3rd print hosts do not have the send options
|
||||
|
||||
const auto& cfg = preset_bundle->printers.get_edited_preset().config;
|
||||
const auto* host_type_opt = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type");
|
||||
// Only simply print support uploading all plates
|
||||
support_print_all = host_type_opt != nullptr && host_type_opt->value == PrintHostType::htSimplyPrint;
|
||||
}
|
||||
|
||||
actions.push_back(ePrintPlate);
|
||||
if (support_print_all)
|
||||
actions.push_back(ePrintAll);
|
||||
if (support_send) {
|
||||
actions.push_back(eSendToPrinter);
|
||||
actions.push_back(eSendToPrinterAll);
|
||||
}
|
||||
if (enable_multi_machine)
|
||||
actions.push_back(ePrintMultiMachine);
|
||||
actions.push_back(eExportSlicedFile);
|
||||
actions.push_back(eExportAllSlicedFile);
|
||||
actions.push_back(eExportGcode);
|
||||
return actions;
|
||||
}
|
||||
|
||||
void MainFrame::select_print_action(PrintSelectType select_type)
|
||||
{
|
||||
m_print_btn->SetLabel(print_select_type_label(select_type));
|
||||
m_print_select = select_type;
|
||||
remember_print_select(select_type);
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
}
|
||||
|
||||
void MainFrame::remember_print_select(PrintSelectType select_type)
|
||||
{
|
||||
if (!wxGetApp().app_config->get_bool("remember_print_action"))
|
||||
return;
|
||||
// AppConfig is marked dirty here and flushed by the regular autosave
|
||||
wxGetApp().app_config->set("last_print_action", print_select_type_key(select_type));
|
||||
}
|
||||
|
||||
bool MainFrame::get_remembered_print_select(PrintSelectType& out) const
|
||||
{
|
||||
if (!wxGetApp().app_config->get_bool("remember_print_action"))
|
||||
return false;
|
||||
const std::string saved = wxGetApp().app_config->get("last_print_action");
|
||||
if (saved.empty())
|
||||
return false;
|
||||
// Orca: only restore an action the current printer actually offers in the dropdown
|
||||
for (PrintSelectType type : available_print_actions()) {
|
||||
if (saved == print_select_type_key(type)) {
|
||||
out = type;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
wxBoxSizer* MainFrame::create_side_tools()
|
||||
{
|
||||
enable_multi_machine = wxGetApp().is_enable_multi_machine();
|
||||
@@ -2016,6 +2136,14 @@ wxBoxSizer* MainFrame::create_side_tools()
|
||||
m_print_btn = new SideButton(print_panel, _L("Print plate"), "");
|
||||
m_print_option_btn = new SideButton(print_panel, "", "sidebutton_dropdown", 0, 14);
|
||||
|
||||
// Orca: restore the last used print/export action if the user opted to remember it
|
||||
PrintSelectType remembered_print_select;
|
||||
if (get_remembered_print_select(remembered_print_select)) {
|
||||
m_print_select = remembered_print_select;
|
||||
m_print_btn->SetLabel(print_select_type_label(remembered_print_select));
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
}
|
||||
|
||||
auto slice_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
slice_sizer->Add(m_slice_option_btn, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(1));
|
||||
slice_sizer->Add(m_slice_btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(1));
|
||||
@@ -2178,193 +2306,15 @@ wxBoxSizer* MainFrame::create_side_tools()
|
||||
m_print_option_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event)
|
||||
{
|
||||
SidePopup* p = new SidePopup(this);
|
||||
|
||||
if (wxGetApp().preset_bundle
|
||||
&& !wxGetApp().preset_bundle->is_bbl_vendor()
|
||||
&& !wxGetApp().app_config->get_bool("use_printer_agents")) {
|
||||
// ThirdParty Buttons
|
||||
SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), "");
|
||||
export_gcode_btn->SetCornerRadius(0);
|
||||
export_gcode_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) {
|
||||
m_print_btn->SetLabel(_L("Export G-code file"));
|
||||
m_print_select = eExportGcode;
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
for (PrintSelectType type : available_print_actions()) {
|
||||
SideButton* btn = new SideButton(p, print_select_type_label(type), "");
|
||||
btn->SetCornerRadius(0);
|
||||
btn->Bind(wxEVT_BUTTON, [this, p, type](wxCommandEvent&) {
|
||||
select_print_action(type);
|
||||
p->Dismiss();
|
||||
});
|
||||
|
||||
// upload and print
|
||||
SideButton* send_gcode_btn = new SideButton(p, _L_CONTEXT("Print", "Verb"), "");
|
||||
send_gcode_btn->SetCornerRadius(0);
|
||||
send_gcode_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) {
|
||||
m_print_btn->SetLabel(_L_CONTEXT("Print", "Verb"));
|
||||
m_print_select = eSendGcode;
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
p->Dismiss();
|
||||
});
|
||||
|
||||
p->append_button(send_gcode_btn);
|
||||
|
||||
// Orca: when the printer accepts a .gcode.3mf (the "Support 3MF as gcode" option),
|
||||
// also offer exporting the sliced .gcode.3mf bundle
|
||||
const auto& printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
||||
const auto* use_3mf_opt = printer_config.option<ConfigOptionBool>("use_3mf");
|
||||
if (use_3mf_opt != nullptr && use_3mf_opt->value) {
|
||||
SideButton* export_sliced_file_btn = new SideButton(p, _L("Export plate sliced file"), "");
|
||||
export_sliced_file_btn->SetCornerRadius(0);
|
||||
export_sliced_file_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) {
|
||||
m_print_btn->SetLabel(_L("Export plate sliced file"));
|
||||
m_print_select = eExportSlicedFile;
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
p->Dismiss();
|
||||
});
|
||||
p->append_button(export_sliced_file_btn);
|
||||
}
|
||||
|
||||
p->append_button(export_gcode_btn);
|
||||
p->append_button(btn);
|
||||
}
|
||||
else {
|
||||
//Orca Slicer Buttons
|
||||
SideButton* print_plate_btn = new SideButton(p, _L("Print plate"), "");
|
||||
print_plate_btn->SetCornerRadius(0);
|
||||
|
||||
SideButton* send_to_printer_btn = new SideButton(p, _L("Send"), "");
|
||||
send_to_printer_btn->SetCornerRadius(0);
|
||||
|
||||
SideButton* export_sliced_file_btn = new SideButton(p, _L("Export plate sliced file"), "");
|
||||
export_sliced_file_btn->SetCornerRadius(0);
|
||||
|
||||
SideButton* export_all_sliced_file_btn = new SideButton(p, _L("Export all sliced file"), "");
|
||||
export_all_sliced_file_btn->SetCornerRadius(0);
|
||||
|
||||
print_plate_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) {
|
||||
m_print_btn->SetLabel(_L("Print plate"));
|
||||
m_print_select = ePrintPlate;
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
p->Dismiss();
|
||||
});
|
||||
|
||||
SideButton* print_all_btn = new SideButton(p, _L("Print all"), "");
|
||||
print_all_btn->SetCornerRadius(0);
|
||||
print_all_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) {
|
||||
m_print_btn->SetLabel(_L("Print all"));
|
||||
m_print_select = ePrintAll;
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
p->Dismiss();
|
||||
});
|
||||
|
||||
send_to_printer_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) {
|
||||
m_print_btn->SetLabel(_L("Send"));
|
||||
m_print_select = eSendToPrinter;
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
p->Dismiss();
|
||||
});
|
||||
|
||||
SideButton* send_to_printer_all_btn = new SideButton(p, _L("Send all"), "");
|
||||
send_to_printer_all_btn->SetCornerRadius(0);
|
||||
send_to_printer_all_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) {
|
||||
m_print_btn->SetLabel(_L("Send all"));
|
||||
m_print_select = eSendToPrinterAll;
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
p->Dismiss();
|
||||
});
|
||||
|
||||
export_sliced_file_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) {
|
||||
m_print_btn->SetLabel(_L("Export plate sliced file"));
|
||||
m_print_select = eExportSlicedFile;
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
p->Dismiss();
|
||||
});
|
||||
|
||||
export_all_sliced_file_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) {
|
||||
m_print_btn->SetLabel(_L("Export all sliced file"));
|
||||
m_print_select = eExportAllSlicedFile;
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
p->Dismiss();
|
||||
});
|
||||
|
||||
bool support_send = true;
|
||||
bool support_print_all = true;
|
||||
|
||||
const auto preset_bundle = wxGetApp().preset_bundle;
|
||||
if (preset_bundle) {
|
||||
if (preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) {
|
||||
// BBL network support everything
|
||||
} else {
|
||||
support_send = false; // All 3rd print hosts do not have the send options
|
||||
|
||||
auto cfg = preset_bundle->printers.get_edited_preset().config;
|
||||
const auto host_type = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type")->value;
|
||||
|
||||
// Only simply print support uploading all plates
|
||||
support_print_all = host_type == PrintHostType::htSimplyPrint;
|
||||
}
|
||||
}
|
||||
|
||||
p->append_button(print_plate_btn);
|
||||
if (support_print_all) {
|
||||
p->append_button(print_all_btn);
|
||||
}
|
||||
if (support_send) {
|
||||
p->append_button(send_to_printer_btn);
|
||||
p->append_button(send_to_printer_all_btn);
|
||||
}
|
||||
if (enable_multi_machine) {
|
||||
SideButton* print_multi_machine_btn = new SideButton(p, _L("Send to Multi-device"), "");
|
||||
print_multi_machine_btn->SetCornerRadius(0);
|
||||
print_multi_machine_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) {
|
||||
m_print_btn->SetLabel(_L("Send to Multi-device"));
|
||||
m_print_select = ePrintMultiMachine;
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
p->Dismiss();
|
||||
});
|
||||
p->append_button(print_multi_machine_btn);
|
||||
}
|
||||
p->append_button(export_sliced_file_btn);
|
||||
p->append_button(export_all_sliced_file_btn);
|
||||
SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), "");
|
||||
export_gcode_btn->SetCornerRadius(0);
|
||||
export_gcode_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) {
|
||||
m_print_btn->SetLabel(_L("Export G-code file"));
|
||||
m_print_select = eExportGcode;
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
p->Dismiss();
|
||||
});
|
||||
p->append_button(export_gcode_btn);
|
||||
}
|
||||
|
||||
p->Popup(m_print_btn);
|
||||
}
|
||||
);
|
||||
@@ -4224,38 +4174,22 @@ void MainFrame::on_config_changed(DynamicPrintConfig* config) const
|
||||
|
||||
void MainFrame::set_print_button_to_default(PrintSelectType select_type)
|
||||
{
|
||||
if (select_type == PrintSelectType::ePrintPlate) {
|
||||
m_print_btn->SetLabel(_L("Print plate"));
|
||||
m_print_select = ePrintPlate;
|
||||
if (m_print_enable)
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
} else if (select_type == PrintSelectType::eSendGcode) {
|
||||
m_print_btn->SetLabel(_L_CONTEXT("Print", "Verb"));
|
||||
m_print_select = eSendGcode;
|
||||
if (m_print_enable)
|
||||
m_print_enable = get_enable_print_status() && can_send_gcode();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
} else if (select_type == PrintSelectType::eExportGcode) {
|
||||
m_print_btn->SetLabel(_L("Export G-code file"));
|
||||
m_print_select = eExportGcode;
|
||||
if (m_print_enable)
|
||||
m_print_enable = get_enable_print_status() && can_send_gcode();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
} else if (select_type == PrintSelectType::eExportSlicedFile) {
|
||||
m_print_btn->SetLabel(_L("Export plate sliced file"));
|
||||
m_print_select = eExportSlicedFile;
|
||||
if (m_print_enable)
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
} else {
|
||||
// unsupport
|
||||
return;
|
||||
}
|
||||
// Orca: keep the user's remembered print/export action instead of resetting it to the computed
|
||||
// default. get_remembered_print_select() already rejects anything this printer does not offer.
|
||||
PrintSelectType remembered;
|
||||
if (get_remembered_print_select(remembered))
|
||||
select_type = remembered;
|
||||
|
||||
if (select_type == eUploadGcode)
|
||||
return; // unsupported: no dropdown entry exists for this action
|
||||
|
||||
m_print_btn->SetLabel(print_select_type_label(select_type));
|
||||
m_print_select = select_type;
|
||||
// get_enable_print_status() already applies can_send_gcode() to the actions that need it
|
||||
if (m_print_enable)
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
}
|
||||
|
||||
void MainFrame::add_to_recent_projects(const wxString& filename)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "Event.hpp"
|
||||
@@ -430,6 +431,14 @@ public:
|
||||
// Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig
|
||||
void on_config_changed(DynamicPrintConfig* cfg) const ;
|
||||
void set_print_button_to_default(PrintSelectType select_type);
|
||||
// Orca: the print/export actions the current printer offers, in the order the dropdown lists them.
|
||||
// The dropdown is built from this, and a remembered action is only restored if it appears here.
|
||||
std::vector<PrintSelectType> available_print_actions() const;
|
||||
// Orca: apply an action picked from the print dropdown to the print button
|
||||
void select_print_action(PrintSelectType select_type);
|
||||
// Orca: remember the user's preferred print/export action across sessions (see "remember_print_action" preference)
|
||||
void remember_print_select(PrintSelectType select_type);
|
||||
bool get_remembered_print_select(PrintSelectType& out) const;
|
||||
|
||||
bool can_save() const;
|
||||
bool can_save_as() const;
|
||||
|
||||
@@ -98,10 +98,7 @@ void MarkdownTip::LoadStyle()
|
||||
ph /= "tooltip/styled.html";
|
||||
_data_dir = false;
|
||||
}
|
||||
auto url = ph.string();
|
||||
std::replace(url.begin(), url.end(), '\\', '/');
|
||||
url = "file:///" + url;
|
||||
_tipView->LoadURL(from_u8(url));
|
||||
_tipView->LoadURL(file_url_from_path(ph));
|
||||
_lastTip.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "MainFrame.hpp"
|
||||
#include "Tab.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/PrintBase.hpp"
|
||||
#include "format.hpp"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
@@ -3084,7 +3085,9 @@ bool NotificationManager::push_notification_data(std::unique_ptr<NotificationMan
|
||||
bool retval = false;
|
||||
if (this->activate_existing(notification.get())) {
|
||||
if (m_initialized && m_imgui_ready) {
|
||||
if (notification->get_type() == NotificationType::SlicingWarning) {
|
||||
// Precise Seam already aggregates all causes; replace it on repeated warning events.
|
||||
if (notification->get_type() == NotificationType::SlicingWarning &&
|
||||
notification->get_data().sub_msg_id != PrintStateBase::SlicingPreciseSeamWarning) {
|
||||
m_pop_notifications.back()->append(notification->get_data().ori_text);
|
||||
} else {
|
||||
m_pop_notifications.back()->update(notification->get_data());
|
||||
|
||||
@@ -546,7 +546,8 @@ void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode *node)
|
||||
{
|
||||
bool is_volume_node = node->GetType() & itVolume;
|
||||
int vol_type = static_cast<int>(node->GetVolumeType());
|
||||
is_volume_node &= (vol_type >= int(ModelVolumeType::MODEL_PART) && vol_type <= int(ModelVolumeType::SUPPORT_ENFORCER));
|
||||
// Extended range to include Precise Seam modifier types
|
||||
is_volume_node &= (vol_type >= int(ModelVolumeType::MODEL_PART) && vol_type <= int(ModelVolumeType::PRECISE_SEAM_NEUTRAL));
|
||||
|
||||
if (!node->has_warning_icon() && !node->has_lock()) {
|
||||
node->SetBitmap(is_volume_node ? (
|
||||
|
||||
@@ -286,7 +286,7 @@ bool OpenGLManager::init_gl(bool popup_error)
|
||||
|
||||
bool valid_version = s_gl_info.is_version_greater_or_equal_to(2, 0);
|
||||
if (!valid_version) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Found opengl version <= 3.2"<< std::endl;
|
||||
BOOST_LOG_TRIVIAL(error) << "Found opengl version < 2.0"<< std::endl;
|
||||
// Complain about the OpenGL version.
|
||||
if (popup_error) {
|
||||
wxString message = from_u8((boost::format(
|
||||
|
||||
+340
-117
@@ -286,6 +286,30 @@ static void set_config_values(DynamicPrintConfig *config, const std::string &key
|
||||
}
|
||||
}
|
||||
|
||||
// Orca: a calibration print sets pressure advance explicitly to 0 on each extruder variant that
|
||||
// has it disabled, so the value stored in the printer does not skew the result. Variants with
|
||||
// pressure advance enabled keep their own value.
|
||||
static void zero_pressure_advance_where_disabled(DynamicPrintConfig *filament_config)
|
||||
{
|
||||
auto enable_pa = filament_config->option<ConfigOptionBools>("enable_pressure_advance");
|
||||
auto pa = filament_config->option<ConfigOptionFloats>("pressure_advance");
|
||||
auto adaptive_pa = filament_config->option<ConfigOptionBools>("adaptive_pressure_advance");
|
||||
if (!enable_pa || !pa || !adaptive_pa || pa->empty() || adaptive_pa->empty())
|
||||
return;
|
||||
// All variant keys share the variant count; widen a short one rather than index past it.
|
||||
if (pa->size() < enable_pa->size())
|
||||
pa->resize(enable_pa->size());
|
||||
if (adaptive_pa->size() < enable_pa->size())
|
||||
adaptive_pa->resize(enable_pa->size());
|
||||
for (size_t variant = 0; variant < enable_pa->size(); ++variant) {
|
||||
if (enable_pa->get_at(variant))
|
||||
continue;
|
||||
enable_pa->values[variant] = true;
|
||||
pa->values[variant] = 0.0;
|
||||
adaptive_pa->values[variant] = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool Plater::has_illegal_filename_characters(const wxString& wxs_name)
|
||||
{
|
||||
std::string name = into_u8(wxs_name);
|
||||
@@ -605,15 +629,50 @@ private:
|
||||
bool m_enabled{false};
|
||||
};
|
||||
|
||||
// The nozzle rows are set in the small body font their "T1" prefix and diameter readout use, so the
|
||||
// combos of a row drop from the Body_14 they are created with to match, instead of towering over
|
||||
// the labels next to them.
|
||||
static void use_nozzle_row_font(ComboBox *combo)
|
||||
{
|
||||
// A read-only combo draws its value with the font of the TextInput it is built on, and its
|
||||
// dropped list carries a font of its own.
|
||||
combo->SetFont(Label::Body_10);
|
||||
combo->GetDropDown().SetFont(Label::Body_10);
|
||||
|
||||
// A row is as tall as the tags of the variant switch: a line of Body_10 text plus the 3 points of
|
||||
// padding they put above and below it. TextInput::messureSize() instead sizes a combo as its
|
||||
// (hidden) text control plus 8, and on macOS that control's best height is a fixed 16 points
|
||||
// whatever font it carries, so the box would keep the height of the Body_14 it was created with.
|
||||
// Handing the control the height the row asks for, less those 8 points, keeps messureSize()'s own
|
||||
// arithmetic landing on the small box however often it re-measures.
|
||||
const int row_height = combo->GetTextExtent("0.4 mm").y + 2 * combo->FromDIP(3);
|
||||
auto *text_ctrl = combo->GetTextCtrl();
|
||||
text_ctrl->SetFont(Label::Body_10);
|
||||
text_ctrl->SetInitialSize(wxSize(text_ctrl->GetBestSize().x, row_height - 8));
|
||||
|
||||
combo->SetMinSize(wxSize(combo->GetMinSize().x, row_height));
|
||||
combo->SetSize(wxSize(combo->GetSize().x, row_height));
|
||||
}
|
||||
|
||||
struct ExtruderGroup : StaticBox
|
||||
{
|
||||
// One nozzle row: the tool prefix ("T1"…"Tn", shown only when the group lists several
|
||||
// extruders) plus the nozzle readout and flow combo of a single extruder. Rows are appended on
|
||||
// demand and never destroyed, so an event handler may capture its row index for the lifetime
|
||||
// of the group.
|
||||
struct NozzleRow
|
||||
{
|
||||
Label * prefix = nullptr;
|
||||
Label * diameter_label = nullptr; // read-only nozzle diameter of this extruder
|
||||
ComboBox * flow = nullptr;
|
||||
};
|
||||
|
||||
ExtruderGroup(wxWindow * parent, int index, wxString const &title);
|
||||
wxBoxSizer * sizer = nullptr;
|
||||
HoverLabel * hover_label = nullptr;
|
||||
wxStaticText* ams_label{nullptr};
|
||||
ScalableButton * btn_edit = nullptr;
|
||||
ComboBox * combo_diameter = nullptr;
|
||||
ComboBox * combo_flow = nullptr;
|
||||
AMSPreview * ams[4] = {nullptr};
|
||||
wxStaticText *ams_not_installed_msg{nullptr};
|
||||
ScalableButton * btn_up{nullptr};
|
||||
@@ -627,6 +686,23 @@ struct ExtruderGroup : StaticBox
|
||||
std::vector<AMSinfo> ams_1;
|
||||
wxString diameter;
|
||||
|
||||
std::vector<NozzleRow> rows; // rows[0] always exists, one row per extruder when multi
|
||||
size_t nozzle_row_count = 0; // rows currently shown
|
||||
wxBoxSizer * row_columns[2] = {}; // row i is dealt into column i % 2
|
||||
int group_index = -1; // ctor index, the extruder a single-row group edits
|
||||
|
||||
// Show `count` nozzle rows, one per extruder, each prefixed with T<index> when count > 1.
|
||||
void SetNozzleRowCount(size_t count);
|
||||
size_t NozzleRowCount() const { return nozzle_row_count; }
|
||||
// Nozzle diameter readout of the shown rows, one value per extruder. A listing card cannot edit
|
||||
// it: the diameter belongs to the machine variant the printer preset selects, not to the extruder.
|
||||
void SetRowDiameters(const std::vector<double> &diameters);
|
||||
|
||||
NozzleRow create_nozzle_row();
|
||||
// Widths the prefix and diameter labels of the visible rows so that the flow combos of a column
|
||||
// line up.
|
||||
void update_row_widths();
|
||||
|
||||
void set_ams_count(int n4, int n1)
|
||||
{
|
||||
if (n4 == ams_n4 && n1 == ams_n1)
|
||||
@@ -657,7 +733,12 @@ struct ExtruderGroup : StaticBox
|
||||
btn_up->msw_rescale();
|
||||
btn_down->msw_rescale();
|
||||
combo_diameter->Rescale();
|
||||
combo_flow->Rescale();
|
||||
for (const NozzleRow &row : rows) {
|
||||
row.prefix->SetFont(Label::Body_10.Bold());
|
||||
row.diameter_label->SetFont(Label::Body_10);
|
||||
row.flow->Rescale();
|
||||
}
|
||||
update_row_widths();
|
||||
for (int i = 0; i < 4; ++i)
|
||||
ams[i]->msw_rescale();
|
||||
}
|
||||
@@ -668,6 +749,10 @@ struct ExtruderGroup : StaticBox
|
||||
hover_label->sys_color_changed();
|
||||
if (btn_edit)
|
||||
btn_edit->SetBackgroundColour(extruder_group_chip_bg());
|
||||
for (const NozzleRow &row : rows) {
|
||||
row.prefix->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
|
||||
row.diameter_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
};
|
||||
@@ -691,11 +776,12 @@ struct Sidebar::priv
|
||||
ScalableButton * btn_edit_printer = nullptr;
|
||||
ScalableButton * btn_connect_printer = nullptr;
|
||||
|
||||
// Nozzle diameter
|
||||
// Printer variant switch, titled "Variant": the combo lists the variants (nozzle sizes) of the
|
||||
// current machine model and picking one switches the printer preset.
|
||||
StaticBox * panel_nozzle_dia = nullptr;
|
||||
Label * label_nozzle_title= nullptr;
|
||||
ComboBox * combo_nozzle_dia = nullptr;
|
||||
Label * label_nozzle_type = nullptr;
|
||||
Label * label_nozzle_type = nullptr; // read-only nozzle material, one-row layouts only
|
||||
|
||||
// Printer - bed
|
||||
StaticBox * panel_printer_bed = nullptr;
|
||||
@@ -809,6 +895,8 @@ struct Sidebar::priv
|
||||
// otherwise reuses the app_config-cached option when the machine's nozzle config is unchanged.
|
||||
std::optional<NozzleOption> get_nozzle_options(MachineObject* obj, int extruder_count, bool support_multi_nozzle, bool is_manual);
|
||||
bool switch_diameter(bool single);
|
||||
// Switch to the printer preset offering `diameter`.
|
||||
bool switch_diameter_to(const wxString &diameter);
|
||||
void update_sync_status(const MachineObject* obj);
|
||||
|
||||
// Filament Track Switch (H2-family accessory): true only when the connected printer is the
|
||||
@@ -918,11 +1006,19 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual)
|
||||
int extruder_count = 0;
|
||||
const bool has_flow_variant = cfg.support_different_extruders(extruder_count);
|
||||
|
||||
panel_nozzle_dia->Show(!has_flow_variant);
|
||||
extruder_single_sizer->Show(has_flow_variant);
|
||||
// ORCA: a non-Bambu printer with several extruders lists one nozzle row per tool; Bambu's own
|
||||
// multi-extruder printers keep the left/right pair above.
|
||||
const bool multi_extruder_rows = !preset_bundle.is_bbl_vendor() && extruder_count > 1;
|
||||
|
||||
// The variant box switches the machine variant. The card below lists the nozzles when there
|
||||
// are flow variants to choose or several extruders to show; otherwise the box says it all.
|
||||
panel_nozzle_dia->Show(true);
|
||||
extruder_single_sizer->Show(has_flow_variant || multi_extruder_rows);
|
||||
single_extruder->SetNozzleRowCount(multi_extruder_rows ? size_t(extruder_count) : 1);
|
||||
} else {
|
||||
panel_nozzle_dia->Show(false);
|
||||
extruder_single_sizer->Show(false);
|
||||
single_extruder->SetNozzleRowCount(1);
|
||||
}
|
||||
|
||||
// ORCA ensure printer section is visible after changing printer from printer selection dialog
|
||||
@@ -1319,7 +1415,7 @@ public:
|
||||
};
|
||||
|
||||
ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title)
|
||||
: StaticBox(parent)
|
||||
: StaticBox(parent), group_index(index)
|
||||
{
|
||||
SetFont(Label::Body_10);
|
||||
SetForegroundColour(wxColour("#CECECE"));
|
||||
@@ -1332,29 +1428,13 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
|
||||
hover_label = new HoverLabel(this, title);
|
||||
hover_label->SetPosition(wxPoint(FromDIP(PRINTER_PANEL_RADIUS), 0)); // position it without putting in a sizer so it will look like title
|
||||
|
||||
// Nozzle
|
||||
// Nozzle. Only the dual left/right cards show this combo: it is how they ask which nozzle the
|
||||
// project should print with. Every other card reads the diameter out as text.
|
||||
auto combo_diameter = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
|
||||
this->combo_diameter = combo_diameter;
|
||||
combo_diameter->SetToolTip(_L("Diameter"));
|
||||
|
||||
// Flow
|
||||
auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
|
||||
combo_flow->GetDropDown().SetUseContentWidth(true);
|
||||
combo_flow->Bind(wxEVT_COMBOBOX, [index, combo_flow](wxCommandEvent &evt) {
|
||||
auto printer_tab = dynamic_cast<TabPrinter *>(wxGetApp().get_tab(Preset::TYPE_PRINTER));
|
||||
NozzleVolumeType volume_type = NozzleVolumeType(intptr_t(combo_flow->GetClientData(evt.GetInt())));
|
||||
printer_tab->set_extruder_volume_type(index, volume_type);
|
||||
auto plater = GUI::wxGetApp().plater();
|
||||
if (plater) {
|
||||
// A new Flow type invalidates the per-filament volume choices stored on the
|
||||
// plates for this extruder; rewrite them so the next apply/grouping sees the
|
||||
// selected volume instead of a stale one.
|
||||
plater->update_filament_volume_map(index, static_cast<int>(volume_type));
|
||||
plater->update_machine_sync_status();
|
||||
}
|
||||
});
|
||||
this->combo_flow = combo_flow;
|
||||
combo_flow->SetToolTip(_L("Flow"));
|
||||
combo_diameter->Show(index >= 0);
|
||||
use_nozzle_row_font(combo_diameter);
|
||||
|
||||
// AMS
|
||||
auto ams_panel = new wxPanel(this, wxID_ANY);
|
||||
@@ -1440,10 +1520,6 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
|
||||
btn_down->Hide();
|
||||
|
||||
wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL);
|
||||
wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
hsizer->Add(combo_diameter, 1, wxRIGHT, FromDIP(5));
|
||||
hsizer->Add(combo_flow , 1);
|
||||
|
||||
vsizer->AddSpacer(FromDIP(16)); // spacing for title and control
|
||||
if (index < 0) {
|
||||
@@ -1452,14 +1528,137 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
|
||||
vsizer->Add(ams_panel, 0, wxEXPAND | wxLEFT | wxRIGHT , FromDIP(5));
|
||||
vsizer->AddSpacer(FromDIP(2));
|
||||
}
|
||||
vsizer->Add(hsizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5));
|
||||
|
||||
// Nozzle rows. The first one is the only one for a printer whose nozzles are not listed
|
||||
// individually; a multi-extruder printer grows one row per tool (see SetNozzleRowCount).
|
||||
// The rows are dealt into two columns, which halves the height of a long tool list; the second
|
||||
// column empties itself when there is a single row, so a one-row card still spans the width.
|
||||
auto rows_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
for (wxBoxSizer *&column : row_columns) {
|
||||
column = new wxBoxSizer(wxVERTICAL);
|
||||
rows_sizer->Add(column, 1, wxEXPAND);
|
||||
}
|
||||
vsizer->Add(rows_sizer, 0, wxEXPAND);
|
||||
sizer = vsizer; // the floating filament-switch icon positions itself against this card
|
||||
SetSizer(vsizer);
|
||||
SetNozzleRowCount(1);
|
||||
|
||||
Layout();
|
||||
|
||||
AMSCountPopupWindow::UpdateAMSCount(index < 0 ? 0 : index, this);
|
||||
}
|
||||
|
||||
ExtruderGroup::NozzleRow ExtruderGroup::create_nozzle_row()
|
||||
{
|
||||
const size_t row_index = rows.size();
|
||||
NozzleRow row;
|
||||
|
||||
row.prefix = new Label(this, Label::Body_10.Bold());
|
||||
row.prefix->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
|
||||
row.prefix->SetLabelText(wxString::Format("T%d", int(row_index) + 1));
|
||||
row.prefix->Hide();
|
||||
|
||||
row.diameter_label = new Label(this, Label::Body_10);
|
||||
row.diameter_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
|
||||
row.diameter_label->Hide();
|
||||
|
||||
auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
|
||||
combo_flow->GetDropDown().SetUseContentWidth(true);
|
||||
combo_flow->Bind(wxEVT_COMBOBOX, [this, row_index, combo_flow](wxCommandEvent &evt) {
|
||||
// A one-row card edits the extruder it was created for, a multi-row card one extruder per row.
|
||||
const int index = nozzle_row_count > 1 ? int(row_index) : group_index;
|
||||
auto printer_tab = dynamic_cast<TabPrinter *>(wxGetApp().get_tab(Preset::TYPE_PRINTER));
|
||||
NozzleVolumeType volume_type = NozzleVolumeType(intptr_t(combo_flow->GetClientData(evt.GetInt())));
|
||||
printer_tab->set_extruder_volume_type(index, volume_type);
|
||||
auto plater = GUI::wxGetApp().plater();
|
||||
if (plater) {
|
||||
// A new Flow type invalidates the per-filament volume choices stored on the
|
||||
// plates for this extruder; rewrite them so the next apply/grouping sees the
|
||||
// selected volume instead of a stale one.
|
||||
plater->update_filament_volume_map(index, static_cast<int>(volume_type));
|
||||
plater->update_machine_sync_status();
|
||||
}
|
||||
});
|
||||
combo_flow->SetToolTip(_L("Flow"));
|
||||
row.flow = combo_flow;
|
||||
use_nozzle_row_font(combo_flow);
|
||||
|
||||
auto row_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
row_sizer->Add(row.prefix, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(3));
|
||||
if (row_index == 0)
|
||||
row_sizer->Add(combo_diameter, 1, wxRIGHT, FromDIP(5));
|
||||
row_sizer->Add(row.diameter_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
|
||||
row_sizer->Add(row.flow, 1);
|
||||
// Row-major: T1 T2 on the first line, T3 T4 on the second, and so on.
|
||||
row_columns[row_index % 2]->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5));
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
void ExtruderGroup::update_row_widths()
|
||||
{
|
||||
// Measured from the rows on screen rather than fixed, so no gap is reserved for a "T10" or a
|
||||
// "0.25 mm" that no row carries while the flow combos of a column still line up.
|
||||
wxSize prefix_width(0, -1);
|
||||
wxSize diameter_width(0, -1);
|
||||
for (const NozzleRow &row : rows) {
|
||||
if (row.prefix->IsShown())
|
||||
prefix_width.x = std::max(prefix_width.x, row.prefix->GetTextExtent(row.prefix->GetLabelText()).x);
|
||||
if (row.diameter_label->IsShown())
|
||||
diameter_width.x = std::max(diameter_width.x, row.diameter_label->GetTextExtent(row.diameter_label->GetLabelText()).x);
|
||||
}
|
||||
for (const NozzleRow &row : rows) {
|
||||
row.prefix->SetMinSize(prefix_width);
|
||||
row.diameter_label->SetMinSize(diameter_width);
|
||||
}
|
||||
}
|
||||
|
||||
void ExtruderGroup::SetRowDiameters(const std::vector<double> &diameters)
|
||||
{
|
||||
for (size_t i = 0; i < std::min(nozzle_row_count, diameters.size()); ++i) {
|
||||
// Spelled like the calibration dialogs spell a nozzle diameter, "0.4 mm".
|
||||
const wxString text = from_u8(get_diameter_string(diameters[i])) + " mm";
|
||||
rows[i].diameter_label->SetLabelText(text);
|
||||
rows[i].diameter_label->SetToolTip(text);
|
||||
}
|
||||
update_row_widths();
|
||||
Layout();
|
||||
}
|
||||
|
||||
void ExtruderGroup::SetNozzleRowCount(size_t count)
|
||||
{
|
||||
const size_t row_count = std::min(count, size_t(MAXIMUM_EXTRUDER_NUMBER));
|
||||
if (row_count == nozzle_row_count)
|
||||
return;
|
||||
nozzle_row_count = row_count;
|
||||
|
||||
// Every row names its tool as soon as there is more than one, so a single-nozzle card keeps the
|
||||
// plain "Nozzle" look. A row is shown by showing its widgets: a sizer item holding a sub-sizer
|
||||
// reports itself as shown while any of its windows is, so hiding the sub-sizer alone would
|
||||
// leave the row in the layout.
|
||||
for (size_t i = 0; i < row_count; ++i) {
|
||||
if (i >= rows.size())
|
||||
rows.push_back(create_nozzle_row());
|
||||
rows[i].prefix->Show(row_count > 1);
|
||||
rows[i].diameter_label->Show(group_index < 0); // the dual left/right cards show combo_diameter
|
||||
// The flow combo's own visibility is decided with its choices, see update_extruder_variant.
|
||||
}
|
||||
for (size_t i = row_count; i < rows.size(); ++i) {
|
||||
rows[i].prefix->Hide();
|
||||
rows[i].diameter_label->Hide();
|
||||
rows[i].flow->Hide();
|
||||
}
|
||||
|
||||
// The rows just changed: re-measure them, then let the card and its containers give it the new room.
|
||||
update_row_widths();
|
||||
Layout();
|
||||
if (wxWindow *parent = GetParent()) {
|
||||
parent->Layout();
|
||||
if (wxWindow *grand_parent = parent->GetParent())
|
||||
grand_parent->Layout();
|
||||
}
|
||||
}
|
||||
|
||||
void ExtruderGroup::update_ams()
|
||||
{
|
||||
static AMSinfo info4;
|
||||
@@ -1588,9 +1787,19 @@ bool Sidebar::priv::switch_diameter(bool single)
|
||||
diameter = diameter_left;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return switch_diameter_to(diameter);
|
||||
}
|
||||
|
||||
bool Sidebar::priv::switch_diameter_to(const wxString &diameter)
|
||||
{
|
||||
// ORCA: Check if the selected diameter matches the current nozzle diameter in the config
|
||||
Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset();
|
||||
// The combo lists printer variants, and the variant of a mixed-nozzle machine ("0.4+0.6") is no
|
||||
// single extruder's diameter, so the preset's own variant answers first.
|
||||
if (printer_preset.config.opt_string("printer_variant") == diameter.ToStdString()) {
|
||||
return true;
|
||||
}
|
||||
auto* nozzle_diameter = dynamic_cast<const ConfigOptionFloats*>(printer_preset.config.option("nozzle_diameter"));
|
||||
if (nozzle_diameter && nozzle_diameter->size() > 0) {
|
||||
auto current_nozzle_dia = get_diameter_string(nozzle_diameter->values[0]);
|
||||
@@ -2686,7 +2895,8 @@ Sidebar::Sidebar(Plater *parent)
|
||||
p->combo_nozzle_dia->wxEvtHandler::ProcessEvent(evt);
|
||||
});
|
||||
|
||||
p->label_nozzle_title = new Label(p->panel_nozzle_dia, _L("Nozzle"), LB_PROPAGATE_MOUSE_EVENT);
|
||||
// "Variant", not "Nozzle": picking one switches the printer preset (see panel_nozzle_dia).
|
||||
p->label_nozzle_title = new Label(p->panel_nozzle_dia, _L("Variant"), LB_PROPAGATE_MOUSE_EVENT);
|
||||
p->label_nozzle_title->SetFont(Label::Body_10);
|
||||
|
||||
p->combo_nozzle_dia = new ComboBox(p->panel_nozzle_dia, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
|
||||
@@ -2695,12 +2905,15 @@ Sidebar::Sidebar(Plater *parent)
|
||||
p->combo_nozzle_dia->SetMinSize(FromDIP(wxSize(PRINTER_PANEL_SIZE.GetWidth() - 4, 26))); // requires a static value in here
|
||||
p->combo_nozzle_dia->SetMaxSize(FromDIP(wxSize(PRINTER_PANEL_SIZE.GetWidth() - 4, 26))); // using -1 with wxEXPAND has issues
|
||||
p->combo_nozzle_dia->Bind(wxEVT_COMBOBOX, [this](auto &e) {
|
||||
auto evt_combo = (*p->single_extruder).combo_diameter;
|
||||
evt_combo->SetSelection(e.GetSelection());
|
||||
wxCommandEvent evt(wxEVT_COMBOBOX, evt_combo->GetId());
|
||||
evt.SetEventObject(evt_combo);
|
||||
evt.SetInt(e.GetSelection());
|
||||
wxPostEvent(evt_combo, evt);
|
||||
// This box is the machine-variant selector, so it switches the printer preset directly
|
||||
// instead of feeding an extruder card's combo. Deferred, because switching preset
|
||||
// rebuilds this very combo.
|
||||
const wxString diameter = p->combo_nozzle_dia->GetValue();
|
||||
p->combo_nozzle_dia->CallAfter([this, diameter]() {
|
||||
p->is_switching_diameter = true;
|
||||
p->switch_diameter_to(diameter);
|
||||
p->is_switching_diameter = false;
|
||||
});
|
||||
e.Skip();
|
||||
});
|
||||
// ORCA paint whole combobox on focus
|
||||
@@ -3490,7 +3703,7 @@ void Sidebar::update_all_preset_comboboxes()
|
||||
wxString url = from_u8(PrintHost::get_print_host_webui(&cfg));
|
||||
wxString apikey;
|
||||
if(url.empty())
|
||||
url = wxString::Format("file://%s/web/orca/missing_connection.html", from_u8(resources_dir()));
|
||||
url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/orca/missing_connection.html");
|
||||
else {
|
||||
const auto host_type = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type")->value;
|
||||
if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint))
|
||||
@@ -3680,8 +3893,16 @@ void Sidebar::update_presets(Preset::Type preset_type)
|
||||
auto diameters = wxGetApp().preset_bundle->printers.diameters_of_selected_printer();
|
||||
auto diameter = printer_preset.config.opt_string("printer_variant");
|
||||
auto extruder_max_nozzle_count = printer_preset.config.option<ConfigOptionIntsNullable>("extruder_max_nozzle_count");
|
||||
auto update_extruder_variant = [printer_model, extruders_def, extruders, nozzle_volumes_def, nozzle_volumes, extruder_variants,diameter,extruder_max_nozzle_count](ExtruderGroup & extruder, int index) {
|
||||
extruder.combo_flow->Clear();
|
||||
auto update_extruder_variant = [printer_model, extruders_def, extruders, nozzle_volumes_def, nozzle_volumes, extruder_variants,diameter,extruder_max_nozzle_count](ExtruderGroup & extruder, size_t row, int index) {
|
||||
ComboBox *combo_flow = extruder.rows[row].flow;
|
||||
combo_flow->Clear();
|
||||
// A profile may leave the per-extruder lists shorter than the nozzle count (they are
|
||||
// padded when the printer is loaded, but a hand-written preset need not be).
|
||||
if (index >= int(extruder_variants->values.size()) || index >= int(extruders->values.size()) ||
|
||||
index >= int(nozzle_volumes->values.size())) {
|
||||
combo_flow->Hide();
|
||||
return;
|
||||
}
|
||||
auto type = extruders_def->enum_labels[extruders->values[index]];
|
||||
int select = -1;
|
||||
for (size_t i = 0; i < nozzle_volumes_def->enum_labels.size(); ++i) {
|
||||
@@ -3691,26 +3912,36 @@ void Sidebar::update_presets(Preset::Type preset_type)
|
||||
if (boost::algorithm::contains(extruder_variants->values[index], type + " " + nozzle_volumes_def->enum_labels[i]) ||
|
||||
(extruder_max_nozzle_count->get_at(index) > 1 && extruder_max_nozzle_count->get_at(index) != ConfigOptionIntsNullable::nil_value() &&
|
||||
nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == nvtHybrid)) {
|
||||
if (nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == NozzleVolumeType::nvtHighFlow &&(diameter == "0.2" ||
|
||||
auto cur_volume_type = nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]);
|
||||
// Defensive: profiles restrict E3D to 0.4 / 0.6; keep it out elsewhere.
|
||||
if (cur_volume_type == NozzleVolumeType::nvtE3DHighFlow && diameter != "0.4" && diameter != "0.6")
|
||||
continue;
|
||||
if (cur_volume_type == NozzleVolumeType::nvtHighFlow && (diameter == "0.2" ||
|
||||
is_skip_high_flow_printer(printer_model)))
|
||||
continue;
|
||||
if (nozzle_volumes->values[index] == i)
|
||||
select = extruder.combo_flow->GetCount();
|
||||
extruder.combo_flow->Append(_L(nozzle_volumes_def->enum_labels[i]), {}, (void*)i);
|
||||
// The client data is the enum value, not the label position: E3D High Flow is 5
|
||||
// but sits at position 4, and a position would write an invalid type.
|
||||
if (nozzle_volumes->values[index] == cur_volume_type)
|
||||
select = combo_flow->GetCount();
|
||||
combo_flow->Append(_L(nozzle_volumes_def->enum_labels[i]), {}, (void*)(intptr_t)cur_volume_type);
|
||||
}
|
||||
}
|
||||
if (select == -1)
|
||||
select = extruder.combo_flow->GetCount() - 1;
|
||||
extruder.combo_flow->SetSelection(select);
|
||||
select = combo_flow->GetCount() - 1;
|
||||
combo_flow->SetSelection(select);
|
||||
// No flow variant for this extruder: the diameter keeps the whole row.
|
||||
combo_flow->Show(combo_flow->GetCount() > 0);
|
||||
};
|
||||
|
||||
auto update_extruder_diameter = [&diameters, &nozzle_diameter](int extruder_index,ExtruderGroup & extruder) {
|
||||
extruder.combo_diameter->Clear();
|
||||
if (extruder_index >= int(nozzle_diameter->values.size()))
|
||||
return;
|
||||
int select = -1;
|
||||
// ORCA get the actual nozzle diameter from printer config
|
||||
auto nozzle_dia = get_diameter_string(nozzle_diameter->values[extruder_index]);
|
||||
// ORCA try to add nozzle diameter from config if list is empty. fixes blank nozzle combo box when preset has no alias
|
||||
if(diameters[0].empty() && !nozzle_dia.empty()){
|
||||
if(!diameters.empty() && diameters[0].empty() && !nozzle_dia.empty()){
|
||||
diameters[0] = nozzle_dia;
|
||||
}
|
||||
// Orca: Check if the actual nozzle diameter exists in the list, if not add it as a custom option
|
||||
@@ -3726,14 +3957,15 @@ void Sidebar::update_presets(Preset::Type preset_type)
|
||||
extruder.diameter = nozzle_dia;
|
||||
};
|
||||
auto image_path = get_cur_select_bed_image();
|
||||
if (is_dual_extruder) {
|
||||
const bool multi_extruder_rows = p->single_extruder->NozzleRowCount() > 1;
|
||||
if (is_dual_extruder && !multi_extruder_rows) {
|
||||
std::string printer_type = printer_preset.get_printer_type(wxGetApp().preset_bundle);
|
||||
p->left_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase)));
|
||||
p->right_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase)));
|
||||
AMSCountPopupWindow::UpdateAMSCount(0, p->left_extruder);
|
||||
AMSCountPopupWindow::UpdateAMSCount(1, p->right_extruder);
|
||||
update_extruder_variant(*p->left_extruder, 0);
|
||||
update_extruder_variant(*p->right_extruder, 1);
|
||||
update_extruder_variant(*p->left_extruder, 0, 0);
|
||||
update_extruder_variant(*p->right_extruder, 0, 1);
|
||||
//if (!p->is_switching_diameter) {
|
||||
update_extruder_diameter(0, *p->left_extruder);
|
||||
update_extruder_diameter(1, *p->right_extruder);
|
||||
@@ -3741,15 +3973,27 @@ void Sidebar::update_presets(Preset::Type preset_type)
|
||||
p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight()));
|
||||
} else {
|
||||
AMSCountPopupWindow::UpdateAMSCount(0, p->single_extruder);
|
||||
update_extruder_variant(*p->single_extruder, 0);
|
||||
for (size_t row = 0; row < p->single_extruder->NozzleRowCount(); ++row)
|
||||
update_extruder_variant(*p->single_extruder, row, int(row));
|
||||
//if (!p->is_switching_diameter)
|
||||
update_extruder_diameter(0, *p->single_extruder);
|
||||
// The card reads the nozzles out, the variant box above is what switches the machine.
|
||||
p->single_extruder->SetRowDiameters(nozzle_diameter->values);
|
||||
if (multi_extruder_rows) {
|
||||
// The hidden left/right cards keep mirroring the first extruders' diameters: the
|
||||
// device sync path (a connected Bambu printer or a printer agent) still reads them.
|
||||
update_extruder_diameter(0, *p->left_extruder);
|
||||
update_extruder_diameter(1, *p->right_extruder);
|
||||
}
|
||||
|
||||
// ORCA sync unified nozzle combo box
|
||||
p->combo_nozzle_dia->Clear();
|
||||
for (size_t i = 0; i < diameters.size(); ++i)
|
||||
p->combo_nozzle_dia->Append(diameters[i], {});
|
||||
p->combo_nozzle_dia->SetSelection((*p->single_extruder).combo_diameter->GetSelection());
|
||||
// Prefer the variant the preset names: a mixed-nozzle machine reads "0.4+0.6", which is
|
||||
// no single extruder's diameter.
|
||||
const int variant = p->combo_nozzle_dia->FindString(diameter);
|
||||
p->combo_nozzle_dia->SetSelection(variant != wxNOT_FOUND ? variant : (*p->single_extruder).combo_diameter->GetSelection());
|
||||
|
||||
// ORCA update nozzle type
|
||||
const auto& full_config = wxGetApp().preset_bundle->full_config();
|
||||
@@ -15749,6 +15993,11 @@ void Plater::import_model_id(wxString download_info)
|
||||
//wxString sError = error.what();
|
||||
}
|
||||
|
||||
// The name comes from the link: reduce it to a plain file name inside the download folder.
|
||||
filename = from_u8(sanitize_file_basename(into_u8(filename)));
|
||||
if (filename.empty())
|
||||
filename = "untitled.3mf";
|
||||
|
||||
bool download_ok = false;
|
||||
int retry_count = 0;
|
||||
const int max_retries = 3;
|
||||
@@ -15790,51 +16039,28 @@ void Plater::import_model_id(wxString download_info)
|
||||
|
||||
msg = _L("Preparing 3MF file...");
|
||||
|
||||
//gets the number of files with the same name
|
||||
std::vector<wxString> vecFiles;
|
||||
bool is_already_exist = false;
|
||||
|
||||
|
||||
target_path = fs::path(wxGetApp().app_config->get("download_path"));
|
||||
|
||||
try
|
||||
{
|
||||
vecFiles.clear();
|
||||
wxString extension = fs::path(filename.wx_str()).extension().c_str();
|
||||
|
||||
|
||||
//check file suffix
|
||||
if (!extension.Contains(".3mf")) {
|
||||
msg = _L("Download failed; unknown file format.");
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = filename.substr(0, filename.length() - extension.length() - 1);
|
||||
|
||||
for (const auto& iter : boost::filesystem::directory_iterator(target_path))
|
||||
{
|
||||
if (boost::filesystem::is_directory(iter.path()))
|
||||
continue;
|
||||
|
||||
wxString sFile = iter.path().filename().string().c_str();
|
||||
if (strstr(sFile.c_str(), name.c_str()) != NULL) {
|
||||
vecFiles.push_back(sFile);
|
||||
}
|
||||
|
||||
if (sFile == filename) is_already_exist = true;
|
||||
}
|
||||
}
|
||||
catch (const std::exception&)
|
||||
{
|
||||
//wxString sError = error.what();
|
||||
//check file suffix
|
||||
wxString extension = fs::path(filename.wx_str()).extension().c_str();
|
||||
if (!extension.Contains(".3mf")) {
|
||||
msg = _L("Download failed; unknown file format.");
|
||||
return;
|
||||
}
|
||||
|
||||
//update filename
|
||||
if (is_already_exist && vecFiles.size() >= 1) {
|
||||
wxString extension = fs::path(filename.wx_str()).extension().c_str();
|
||||
wxString name = filename.substr(0, filename.length() - extension.length());
|
||||
filename = wxString::Format("%s(%d)%s", name, vecFiles.size() + 1, extension).ToStdString();
|
||||
//never replace an existing file
|
||||
std::string unused_filename;
|
||||
try {
|
||||
if (!find_unused_filename(target_path, into_u8(filename), {}, unused_filename))
|
||||
unused_filename.clear();
|
||||
} catch (const std::exception&) {
|
||||
unused_filename.clear();
|
||||
}
|
||||
if (unused_filename.empty()) {
|
||||
msg = _L("Importing to Orca Slicer failed. Please download the file and manually import it.");
|
||||
return;
|
||||
}
|
||||
filename = from_u8(unused_filename);
|
||||
|
||||
|
||||
msg = _L("Downloading project...");
|
||||
@@ -15846,10 +16072,6 @@ void Plater::import_model_id(wxString download_info)
|
||||
boost::uuids::uuid uuid = boost::uuids::random_generator()();
|
||||
std::string unique = to_string(uuid).substr(0, 6);
|
||||
|
||||
if (filename.empty()) {
|
||||
filename = "untitled.3mf";
|
||||
}
|
||||
|
||||
//target_path /= (boost::format("%1%_%2%.3mf") % filename % unique).str();
|
||||
target_path /= fs::path(filename.wc_str());
|
||||
|
||||
@@ -15898,13 +16120,26 @@ void Plater::import_model_id(wxString download_info)
|
||||
cont = false;
|
||||
}
|
||||
})
|
||||
.on_complete([&cont, &download_ok, tmp_path, target_path](std::string body, unsigned /* http_status */) {
|
||||
.on_complete([&cont, &download_ok, &msg, tmp_path, &target_path](std::string body, unsigned /* http_status */) {
|
||||
fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc);
|
||||
file.write(body.c_str(), body.size());
|
||||
file.close();
|
||||
fs::rename(tmp_path, target_path);
|
||||
cont = false;
|
||||
download_ok = true;
|
||||
try {
|
||||
// Another file may have taken the name while downloading.
|
||||
std::string unused_filename;
|
||||
if (find_unused_filename(target_path.parent_path(), target_path.filename().string(), {}, unused_filename)) {
|
||||
target_path = target_path.parent_path() / unused_filename;
|
||||
fs::rename(tmp_path, target_path);
|
||||
download_ok = true;
|
||||
return;
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "import_model_id: failed to move the download into place: " << e.what();
|
||||
}
|
||||
boost::system::error_code ec;
|
||||
fs::remove(tmp_path, ec);
|
||||
msg = _L("Importing to Orca Slicer failed. Please download the file and manually import it.");
|
||||
}).perform_sync();
|
||||
|
||||
// for break while
|
||||
@@ -16913,11 +17148,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params)
|
||||
set_config_values<double, ConfigOptionFloatsNullable>(print_config, "default_jerk", 0);
|
||||
}
|
||||
|
||||
if (!filament_config->option<ConfigOptionBools>("enable_pressure_advance")->get_at(0)) {
|
||||
set_config_values<bool, ConfigOptionBools>(filament_config, "enable_pressure_advance", true);
|
||||
set_config_values<double, ConfigOptionFloatsNullable>(filament_config, "pressure_advance", 0.0);
|
||||
set_config_values<bool, ConfigOptionBools>(filament_config, "adaptive_pressure_advance", false);
|
||||
}
|
||||
zero_pressure_advance_where_disabled(filament_config);
|
||||
|
||||
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
|
||||
printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false});
|
||||
@@ -16979,11 +17210,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params)
|
||||
set_config_values<double, ConfigOptionFloatsNullable>(print_config, "default_jerk", 0);
|
||||
}
|
||||
|
||||
if (!filament_config->option<ConfigOptionBools>("enable_pressure_advance")->get_at(0)) {
|
||||
set_config_values<bool, ConfigOptionBools>(filament_config, "enable_pressure_advance", true);
|
||||
set_config_values<double, ConfigOptionFloatsNullable>(filament_config, "pressure_advance", 0.0);
|
||||
set_config_values<bool, ConfigOptionBools>(filament_config, "adaptive_pressure_advance", false);
|
||||
}
|
||||
zero_pressure_advance_where_disabled(filament_config);
|
||||
|
||||
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
|
||||
printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false});
|
||||
@@ -17045,11 +17272,7 @@ void Plater::Calib_Cornering(const Calib_Params& params)
|
||||
set_config_values<double, ConfigOptionFloatsNullable>(print_config, "default_jerk", 0);
|
||||
}
|
||||
|
||||
if (!filament_config->option<ConfigOptionBools>("enable_pressure_advance")->get_at(0)) {
|
||||
set_config_values<bool, ConfigOptionBools>(filament_config, "enable_pressure_advance", true);
|
||||
set_config_values<double, ConfigOptionFloatsNullable>(filament_config, "pressure_advance", 0.0);
|
||||
set_config_values<bool, ConfigOptionBools>(filament_config, "adaptive_pressure_advance", false);
|
||||
}
|
||||
zero_pressure_advance_where_disabled(filament_config);
|
||||
|
||||
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
|
||||
printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{true});
|
||||
|
||||
@@ -1830,7 +1830,10 @@ void PreferencesDialog::create_items()
|
||||
|
||||
auto item_mix_print_high_low_temperature = create_item_checkbox(_L("Remove mixed temperature restriction"), _L("With this option enabled, you can print materials with a large temperature difference together."), "enable_high_low_temp_mixed_printing");
|
||||
g_sizer->Add(item_mix_print_high_low_temperature);
|
||||
|
||||
|
||||
auto item_remember_print_action = create_item_checkbox(_L("Remember last print action"), _L("If enabled, OrcaSlicer will remember the last selected option in the print button's dropdown (e.g. Print, Export plate sliced file, Export G-code file) and use it as the default on next startup."), "remember_print_action");
|
||||
g_sizer->Add(item_remember_print_action);
|
||||
|
||||
//// CONTROL > Camera
|
||||
g_sizer->Add(create_item_title(_L("Camera")), 1, wxEXPAND);
|
||||
|
||||
|
||||
@@ -1793,6 +1793,7 @@ wxString PrinterPartsDialog::GetString(NozzleFlowType nozzle_flow_type) const {
|
||||
case Slic3r::S_FLOW: return _L("Standard");
|
||||
case Slic3r::H_FLOW: return _L("High flow");
|
||||
case Slic3r::U_FLOW: return _L("TPU High flow");
|
||||
case Slic3r::E_FLOW: return _L("E3D High Flow");
|
||||
default: break;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,9 +57,7 @@ PrivacyUpdateDialog::PrivacyUpdateDialog(wxWindow* parent, wxWindowID id, const
|
||||
|
||||
fs::path ph(resources_dir());
|
||||
ph /= "tooltip/privacyupdate.html";
|
||||
m_host_url = ph.string();
|
||||
std::replace(m_host_url.begin(), m_host_url.end(), '\\', '/');
|
||||
m_host_url = "file:///" + m_host_url;
|
||||
m_host_url = into_u8(file_url_from_path(ph));
|
||||
m_vebview_release_note->LoadURL(from_u8(m_host_url));
|
||||
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
|
||||
|
||||
|
||||
@@ -45,10 +45,10 @@ const std::vector<std::string> license_list = {
|
||||
ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style) : wxPanel(parent, id, pos, size, style)
|
||||
{
|
||||
SetBackgroundColour(*wxWHITE);
|
||||
m_project_home_url = wxString::Format("file://%s/web/model/index.html", from_u8(resources_dir()));
|
||||
m_project_home_url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/model/index.html");
|
||||
wxString strlang = wxGetApp().current_language_code_safe();
|
||||
if (strlang != "")
|
||||
m_project_home_url = wxString::Format("file://%s/web/model/index.html?lang=%s", from_u8(resources_dir()), strlang);
|
||||
m_project_home_url += "?lang=" + strlang;
|
||||
|
||||
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
@@ -293,10 +293,8 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt)
|
||||
if (!accessory_path.empty()) {
|
||||
std::string decode_path = wxGetApp().url_decode(accessory_path.ToStdString());
|
||||
fs::path path(decode_path);
|
||||
|
||||
if (fs::exists(path)) {
|
||||
wxLaunchDefaultApplication(path.wstring(), 0);
|
||||
}
|
||||
if (!desktop_open_project_attachment(this, path))
|
||||
BOOST_LOG_TRIVIAL(warning) << "open_3mf_accessory: not opening " << decode_path;
|
||||
}
|
||||
}
|
||||
else if (strCmd == "request_3mf_info") {
|
||||
|
||||
@@ -512,7 +512,7 @@ void UpdateVersionDialog::update_version_info(wxString release_note, wxString ve
|
||||
out_buf->append(text, size);
|
||||
}, (void*) &html_source, MD_DIALECT_GITHUB | MD_FLAG_STRIKETHROUGH | MD_FLAG_WIKILINKS, 0);
|
||||
html_source.append("</body></html>");
|
||||
m_vebview_release_note->LoadURL("file://" + (boost::filesystem::path (resources_dir()) / "web/guide/0/index.html").string());
|
||||
m_vebview_release_note->LoadURL(file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/0/index.html"));
|
||||
|
||||
SetMinSize(GetSize());
|
||||
SetMaxSize(GetSize());
|
||||
|
||||
@@ -76,6 +76,9 @@ std::string get_nozzle_volume_type_cloud_string(NozzleVolumeType nozzle_volume_t
|
||||
else if (nozzle_volume_type == NozzleVolumeType::nvtTPUHighFlow) {
|
||||
return "tpu_high_flow";
|
||||
}
|
||||
else if (nozzle_volume_type == NozzleVolumeType::nvtE3DHighFlow) {
|
||||
return "e3d_high_flow";
|
||||
}
|
||||
else if (nozzle_volume_type == NozzleVolumeType::nvtHybrid) {
|
||||
// to be supported
|
||||
return "hybrid_flow";
|
||||
|
||||
@@ -67,7 +67,7 @@ bool Selection::Clipboard::is_sla_compliant() const
|
||||
return false;
|
||||
|
||||
for (const ModelVolume* v : o->volumes) {
|
||||
if (v->is_modifier())
|
||||
if (v->is_modifier() || v->is_precise_seam()) // Precise Seam not supported in SLA
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1516,6 +1516,8 @@ bool SyncAmsInfoDialog::is_nozzle_type_match(DevExtderSystem data, wxString &err
|
||||
used_extruders_flow[used_extruders[i]] = "Standard";
|
||||
} else if (nozzle_volume_type == NozzleVolumeType::nvtTPUHighFlow) {
|
||||
used_extruders_flow[used_extruders[i]] = "TPU High Flow";
|
||||
} else if (nozzle_volume_type == NozzleVolumeType::nvtE3DHighFlow) {
|
||||
used_extruders_flow[used_extruders[i]] = "E3D High Flow";
|
||||
} else {
|
||||
used_extruders_flow[used_extruders[i]] = "High Flow";
|
||||
}
|
||||
@@ -1533,6 +1535,8 @@ bool SyncAmsInfoDialog::is_nozzle_type_match(DevExtderSystem data, wxString &err
|
||||
flow_type_of_machine.push_back("Standard");
|
||||
} else if (it->GetNozzleFlowType() == NozzleFlowType::U_FLOW) {
|
||||
flow_type_of_machine.push_back("TPU High Flow");
|
||||
} else if (it->GetNozzleFlowType() == NozzleFlowType::E_FLOW) {
|
||||
flow_type_of_machine.push_back("E3D High Flow");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+63
-31
@@ -506,7 +506,7 @@ void Tab::create_preset_tab()
|
||||
|
||||
if (dynamic_cast<TabPrinter *>(this) || dynamic_cast<TabPrint *>(this)) {
|
||||
m_extruder_switch = new MultiSwitchButton(panel);
|
||||
m_extruder_switch->SetMaxSize({em_unit(this) * 40, -1});
|
||||
m_extruder_switch->SetFitToOptions();
|
||||
m_extruder_switch->Bind(wxCUSTOMEVT_MULTISWITCH_SELECTION, [this](auto &evt) {
|
||||
evt.Skip();
|
||||
int selection = evt.GetInt();
|
||||
@@ -539,7 +539,8 @@ void Tab::create_preset_tab()
|
||||
auto right_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
m_variant_sizer->AddStretchSpacer(1);
|
||||
m_variant_sizer->Add(m_extruder_switch, 0, wxALIGN_CENTER, 0);
|
||||
// Orca: proportion 1 lets a narrow row squeeze the switch, which then scrolls its buttons.
|
||||
m_variant_sizer->Add(m_extruder_switch, 1, wxALIGN_CENTER, 0);
|
||||
m_variant_sizer->Add(right_sizer, 1, wxALIGN_CENTER);
|
||||
right_sizer->AddStretchSpacer(1);
|
||||
right_sizer->Add(m_extruder_sync_box, 0, wxALIGN_CENTER | wxRIGHT, m_em_unit);
|
||||
@@ -4414,15 +4415,15 @@ void TabFilament::build()
|
||||
optgroup->append_single_option_line("pellet_flow_coefficient", "printer_basic_information_advanced#pellet-modded-printer");
|
||||
optgroup->append_single_option_line("filament_flow_ratio", "material_flow_ratio_and_pressure_advance#flow-ratio", 0);
|
||||
|
||||
optgroup->append_single_option_line("enable_pressure_advance", "material_flow_ratio_and_pressure_advance#pressure-advance");
|
||||
optgroup->append_single_option_line("pressure_advance", "material_flow_ratio_and_pressure_advance#pressure-advance");
|
||||
optgroup->append_single_option_line("enable_pressure_advance", "material_flow_ratio_and_pressure_advance#pressure-advance", 0);
|
||||
optgroup->append_single_option_line("pressure_advance", "material_flow_ratio_and_pressure_advance#pressure-advance", 0);
|
||||
|
||||
// Orca: adaptive pressure advance and calibration model
|
||||
optgroup->append_single_option_line("adaptive_pressure_advance", "material_flow_ratio_and_pressure_advance#enable-adaptive-pressure-advance-beta");
|
||||
optgroup->append_single_option_line("adaptive_pressure_advance_overhangs", "material_flow_ratio_and_pressure_advance#enable-adaptive-pressure-advance-for-overhangs-beta");
|
||||
optgroup->append_single_option_line("adaptive_pressure_advance_bridges", "material_flow_ratio_and_pressure_advance#pressure-advance-for-bridges");
|
||||
optgroup->append_single_option_line("adaptive_pressure_advance", "material_flow_ratio_and_pressure_advance#enable-adaptive-pressure-advance-beta", 0);
|
||||
optgroup->append_single_option_line("adaptive_pressure_advance_overhangs", "material_flow_ratio_and_pressure_advance#enable-adaptive-pressure-advance-for-overhangs-beta", 0);
|
||||
optgroup->append_single_option_line("adaptive_pressure_advance_bridges", "material_flow_ratio_and_pressure_advance#pressure-advance-for-bridges", 0);
|
||||
|
||||
Option option = optgroup->get_option("adaptive_pressure_advance_model");
|
||||
Option option = optgroup->get_option("adaptive_pressure_advance_model", 0);
|
||||
option.opt.full_width = true;
|
||||
option.opt.is_code = true;
|
||||
option.opt.height = 15;
|
||||
@@ -4431,7 +4432,7 @@ void TabFilament::build()
|
||||
DynamicPrintConfig& filament_config = m_preset_bundle->filaments.get_edited_preset().config;
|
||||
|
||||
update_dirty();
|
||||
if (opt_key == "adaptive_pressure_advance_model")
|
||||
if (opt_key.substr(0, opt_key.find('#')) == "adaptive_pressure_advance_model")
|
||||
m_config_manipulation.check_adaptive_pressure_advance_model(&filament_config);
|
||||
|
||||
on_value_change(opt_key, value);
|
||||
@@ -4828,8 +4829,11 @@ void TabFilament::toggle_options()
|
||||
}
|
||||
if (m_active_page->title() == L("Filament"))
|
||||
{
|
||||
bool pa = m_config->opt_bool("enable_pressure_advance", 0);
|
||||
toggle_option("pressure_advance", pa);
|
||||
const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0;
|
||||
const unsigned int variant_idx = (unsigned int) std::max(selection, 0);
|
||||
|
||||
bool pa = m_config->opt_bool("enable_pressure_advance", variant_idx);
|
||||
toggle_option("pressure_advance", pa, 0);
|
||||
|
||||
//Orca: Enable the plates that should be visible when multi bed support is enabled or a BBL printer is selected; otherwise, enable only the plate visible for the selected bed type.
|
||||
DynamicConfig& proj_cfg = m_preset_bundle->project_config;
|
||||
@@ -4857,12 +4861,12 @@ void TabFilament::toggle_options()
|
||||
// Orca: adaptive pressure advance and calibration model
|
||||
// If PA is not enabled, disable adaptive pressure advance and hide the model section
|
||||
// If adaptive PA is not enabled, hide the adaptive PA model section
|
||||
toggle_option("adaptive_pressure_advance", pa);
|
||||
toggle_option("adaptive_pressure_advance_overhangs", pa);
|
||||
bool has_adaptive_pa = m_config->opt_bool("adaptive_pressure_advance", 0);
|
||||
toggle_line("adaptive_pressure_advance_overhangs", has_adaptive_pa && pa);
|
||||
toggle_line("adaptive_pressure_advance_model", has_adaptive_pa && pa);
|
||||
toggle_line("adaptive_pressure_advance_bridges", has_adaptive_pa && pa);
|
||||
toggle_option("adaptive_pressure_advance", pa, 0);
|
||||
toggle_option("adaptive_pressure_advance_overhangs", pa, 0);
|
||||
bool has_adaptive_pa = m_config->opt_bool("adaptive_pressure_advance", variant_idx);
|
||||
toggle_line("adaptive_pressure_advance_overhangs", has_adaptive_pa && pa, 0);
|
||||
toggle_line("adaptive_pressure_advance_model", has_adaptive_pa && pa, 0);
|
||||
toggle_line("adaptive_pressure_advance_bridges", has_adaptive_pa && pa, 0);
|
||||
|
||||
bool is_pellet_printer = printer_cfg.opt_bool("pellet_modded_printer");
|
||||
toggle_line("pellet_flow_coefficient", is_pellet_printer);
|
||||
@@ -4870,8 +4874,6 @@ void TabFilament::toggle_options()
|
||||
|
||||
toggle_line("activate_chamber_temp_control", printer_cfg.opt_bool("support_chamber_temp_control"));
|
||||
|
||||
const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0;
|
||||
const unsigned int variant_idx = (unsigned int) std::max(selection, 0);
|
||||
std::string volumetric_speed_cos = m_config->opt_string("volumetric_speed_coefficients", variant_idx);
|
||||
bool enable_fit = volumetric_speed_cos != "0 0 0 0 0 0";
|
||||
toggle_option("filament_adaptive_volumetric_speed", enable_fit, 256 + variant_idx);
|
||||
@@ -5204,7 +5206,6 @@ void TabPrinter::build_fff()
|
||||
optgroup->append_single_option_line("adaptive_bed_mesh_margin", "printer_basic_information_adaptive_bed_mesh#mesh-margin");
|
||||
|
||||
optgroup = page->new_optgroup(L("Accessory"), "param_accessory");
|
||||
optgroup->append_single_option_line("nozzle_type", "printer_basic_information_accessory#nozzle-type", 0);
|
||||
optgroup->append_single_option_line("nozzle_hrc", "printer_basic_information_accessory#nozzle-hrc");
|
||||
optgroup->append_single_option_line("auxiliary_fan", "printer_basic_information_accessory#auxiliary-part-cooling-fan");
|
||||
optgroup->append_single_option_line("fan_direction");
|
||||
@@ -5719,6 +5720,7 @@ if (is_marlin_flavor)
|
||||
|
||||
auto optgroup = page->new_optgroup(L("Basic information"), L"param_information", -1, true);
|
||||
optgroup->append_single_option_line("nozzle_diameter", "printer_extruder_basic_information#nozzle-diameter", extruder_idx);
|
||||
optgroup->append_single_option_line("nozzle_type", "printer_basic_information_accessory#nozzle-type", extruder_idx);
|
||||
//optgroup->append_single_option_line("nozzle_volume_type", "", extruder_idx);
|
||||
|
||||
optgroup->append_single_option_line("nozzle_volume", "printer_extruder_basic_information#nozzle-volume", extruder_idx);
|
||||
@@ -5730,7 +5732,7 @@ if (is_marlin_flavor)
|
||||
optgroup->m_on_change = [this, extruder_idx](const t_config_option_key& opt_key, boost::any value)
|
||||
{
|
||||
bool is_SEMM = m_config->opt_bool("single_extruder_multi_material");
|
||||
if (is_SEMM && m_extruders_count > 1 && opt_key.find_first_of("nozzle_diameter") != std::string::npos)
|
||||
if (is_SEMM && m_extruders_count > 1 && boost::starts_with(opt_key, "nozzle_diameter"))
|
||||
{
|
||||
SuppressBackgroundProcessingUpdate sbpu;
|
||||
const double new_nd = boost::any_cast<double>(value);
|
||||
@@ -8172,7 +8174,7 @@ void Tab::update_extruder_variants(int extruder_id, bool reload)
|
||||
|
||||
// Orca: a non-Bambu dual-nozzle printer has two extruders but a single variant column, so
|
||||
// the nozzle switch and sync button have nothing to act on. Only enable with real variants.
|
||||
if (extruder_nums == 2 && m_preset_bundle->support_different_extruders()) {
|
||||
if (extruder_nums >= 2 && m_preset_bundle->support_different_extruders()) {
|
||||
auto options = generate_extruder_options();
|
||||
m_extruder_switch->SetOptions(options);
|
||||
|
||||
@@ -8219,6 +8221,25 @@ void Tab::update_extruder_variants(int extruder_id, bool reload)
|
||||
}
|
||||
}
|
||||
|
||||
// The variant switch tags are the narrowest place a volume type is named, so they abbreviate it;
|
||||
// the flow combo boxes and the Printer tab's parameter labels keep the full names.
|
||||
static wxString short_nozzle_volume_name(const std::string &volume_name)
|
||||
{
|
||||
// Hybrid has no entry on purpose: it is never a variant string, and the switch lists its two
|
||||
// sub-nozzle types as separate tags (see generate_extruder_options).
|
||||
if (volume_name == "Standard")
|
||||
return "SF";
|
||||
if (volume_name == "High Flow")
|
||||
return "HF";
|
||||
if (volume_name == "TPU High Flow")
|
||||
return "TPU HF";
|
||||
if (volume_name == "E3D High Flow")
|
||||
return "E3D HF";
|
||||
if (volume_name == "Extra High Flow")
|
||||
return "XHF";
|
||||
return from_u8(volume_name);
|
||||
}
|
||||
|
||||
std::vector<wxString> Tab::generate_extruder_options()
|
||||
{
|
||||
std::vector<wxString> options;
|
||||
@@ -8259,7 +8280,7 @@ std::vector<wxString> Tab::generate_extruder_options()
|
||||
nozzle = "";
|
||||
}
|
||||
}
|
||||
options.push_back(wxString::Format(_L("%s: %s"), _L(drive), _L(nozzle)));
|
||||
options.push_back(wxString::Format(_L("%s: %s"), _L(drive), short_nozzle_volume_name(nozzle)));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -8273,18 +8294,22 @@ std::vector<wxString> Tab::generate_extruder_options()
|
||||
}
|
||||
|
||||
std::string pt = m_preset_bundle->printers.get_edited_preset().get_printer_type(m_preset_bundle);
|
||||
// Orca: the main/deputy toolhead names describe a dual-nozzle printer, where extruder 0 is the
|
||||
// left (deputy) and extruder 1 the right (main) nozzle. From three extruders on the tools are
|
||||
// interchangeable, so name them by index instead of repeating one side.
|
||||
for (int i = 0; i < extruder_nums; ++i) {
|
||||
int ext_id = (i == 0) ? DEPUTY_EXTRUDER_ID : MAIN_EXTRUDER_ID;
|
||||
wxString extruder_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(
|
||||
pt, ext_id, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase, true));
|
||||
wxString extruder_name = extruder_nums > 2 ? wxString::Format("T%d", i + 1) :
|
||||
_L(DevPrinterConfigUtil::get_toolhead_display_name(
|
||||
pt, (i == 0) ? DEPUTY_EXTRUDER_ID : MAIN_EXTRUDER_ID,
|
||||
ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase, true));
|
||||
NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volumes->values[i]);
|
||||
|
||||
if (volume_type == NozzleVolumeType::nvtHybrid) {
|
||||
options.push_back(wxString::Format(_L("%s: %s"), extruder_name, _L("Standard")));
|
||||
options.push_back(wxString::Format(_L("%s: %s"), extruder_name, _L("High Flow")));
|
||||
options.push_back(wxString::Format(_L("%s: %s"), extruder_name, short_nozzle_volume_name(get_nozzle_volume_type_string(NozzleVolumeType::nvtStandard))));
|
||||
options.push_back(wxString::Format(_L("%s: %s"), extruder_name, short_nozzle_volume_name(get_nozzle_volume_type_string(NozzleVolumeType::nvtHighFlow))));
|
||||
} else {
|
||||
wxString volume_name = get_nozzle_volume_type_name(volume_type);
|
||||
options.push_back(wxString::Format(_L("%s: %s"), extruder_name, volume_name));
|
||||
options.push_back(wxString::Format(_L("%s: %s"), extruder_name,
|
||||
short_nozzle_volume_name(get_nozzle_volume_type_string(volume_type))));
|
||||
}
|
||||
}
|
||||
return options;
|
||||
@@ -8315,7 +8340,10 @@ bool Tab::get_extruder_sync_enable_state(int extruder_id)
|
||||
Preset& printer_preset = m_preset_bundle->printers.get_edited_preset();
|
||||
auto nozzle_volumes = m_preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type");
|
||||
auto extruders = printer_preset.config.option<ConfigOptionEnumsGeneric>("extruder_type");
|
||||
if (nozzle_volumes->values.size() < 2 || extruders->values.size() < 2) {
|
||||
// Orca: every rule below describes the two toolheads of a dual-nozzle printer as left/right.
|
||||
// A printer with any other extruder count has no single counterpart to copy to, so it gets no
|
||||
// sync button (a toolchanger would need a target to be chosen).
|
||||
if (nozzle_volumes->values.size() != 2 || extruders->values.size() != 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -8456,6 +8484,10 @@ void Tab::sync_excluder()
|
||||
ExtruderType(extruders->values[extruder_id]), nozzle_type, variant_keys.second, stride);
|
||||
};
|
||||
int active_index = get_current_active_extruder();
|
||||
// The button copies to the other toolhead, so it is only offered when that other one exists;
|
||||
// without this the `1 - active_index` below would index an extruder that is not there.
|
||||
if (!get_extruder_sync_enable_state(active_index))
|
||||
return;
|
||||
auto active_nozzle = get_actual_nozzle_volume_type(active_index);
|
||||
int from_index = get_index_for_extruder(active_index, active_nozzle);
|
||||
int dest_index = get_index_for_extruder(1 - active_index, active_nozzle);
|
||||
|
||||
@@ -31,14 +31,15 @@ int UserManager::parse_json(std::string payload)
|
||||
{
|
||||
bool restored_json = false;
|
||||
json j;
|
||||
json j_pre = json::parse(payload);
|
||||
if (j_pre.empty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
//bind/unbind
|
||||
|
||||
try {
|
||||
json j_pre = json::parse(payload);
|
||||
if (j_pre.empty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (j_pre.contains("bind")) {
|
||||
if (j_pre["bind"].contains("command")) {
|
||||
|
||||
|
||||
@@ -38,9 +38,7 @@ DownPluginFrame::DownPluginFrame(GUI_App *pGUI) : wxDialog((wxWindow *) (pGUI->m
|
||||
|
||||
// set the frame icon
|
||||
wxBoxSizer *topsizer = new wxBoxSizer(wxVERTICAL);
|
||||
wxString TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/6/index.html").make_preferred().string());
|
||||
|
||||
TargetUrl = "file://" + TargetUrl;
|
||||
wxString TargetUrl = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/6/index.html");
|
||||
|
||||
// Create the webview
|
||||
m_browser = WebView::CreateWebView(this, TargetUrl);
|
||||
|
||||
@@ -218,37 +218,38 @@ wxString GuideFrame::SetStartPage(GuidePage startpage, bool load)
|
||||
m_page = startpage;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(" enter, load=%1%, start_page=%2%")%load%int(startpage);
|
||||
//wxLogMessage("GUIDE: webpage_1 %s", (boost::filesystem::path(resources_dir()) / "web\\guide\\1\\index.html").make_preferred().string().c_str() );
|
||||
wxString TargetUrl = from_u8( (boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=1").make_preferred().string() );
|
||||
const wxString guide_url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/0/index.html");
|
||||
wxString TargetUrl = guide_url + "?target=1";
|
||||
//wxLogMessage("GUIDE: webpage_2 %s", TargetUrl.mb_str());
|
||||
|
||||
if (startpage == BBL_WELCOME){
|
||||
SetTitle(_L("Setup Wizard"));
|
||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=1").make_preferred().string());
|
||||
TargetUrl = guide_url + "?target=1";
|
||||
} else if (startpage == BBL_REGION) {
|
||||
SetTitle(_L("Setup Wizard"));
|
||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=11").make_preferred().string());
|
||||
TargetUrl = guide_url + "?target=11";
|
||||
} else if (startpage == BBL_MODELS) {
|
||||
SetTitle(_L("Setup Wizard"));
|
||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string());
|
||||
TargetUrl = guide_url + "?target=21";
|
||||
} else if (startpage == BBL_FILAMENTS) {
|
||||
SetTitle(_L("Setup Wizard"));
|
||||
|
||||
int nSize = m_ProfileJson["model"].size();
|
||||
|
||||
if (nSize>0)
|
||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=22").make_preferred().string());
|
||||
TargetUrl = guide_url + "?target=22";
|
||||
else
|
||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string());
|
||||
TargetUrl = guide_url + "?target=21";
|
||||
} else if (startpage == BBL_FILAMENT_ONLY) {
|
||||
SetTitle("");
|
||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=23").make_preferred().string());
|
||||
TargetUrl = guide_url + "?target=23";
|
||||
} else if (startpage == BBL_MODELS_ONLY) {
|
||||
SetTitle("");
|
||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=24").make_preferred().string());
|
||||
TargetUrl = guide_url + "?target=24";
|
||||
}
|
||||
else {
|
||||
SetTitle(_L("Setup Wizard"));
|
||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string());
|
||||
TargetUrl = guide_url + "?target=21";
|
||||
}
|
||||
|
||||
wxString strlang = wxGetApp().current_language_code_safe();
|
||||
@@ -256,7 +257,6 @@ wxString GuideFrame::SetStartPage(GuidePage startpage, bool load)
|
||||
if (strlang != "")
|
||||
TargetUrl = wxString::Format("%s&lang=%s", w2s(TargetUrl), strlang);
|
||||
|
||||
TargetUrl = "file://" + TargetUrl;
|
||||
if (load)
|
||||
load_url(TargetUrl);
|
||||
|
||||
@@ -1284,7 +1284,7 @@ bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_r
|
||||
entry["vendor"] = vp.id;
|
||||
entry["nozzle_diameter"] = nozzle_str;
|
||||
entry["materials"] = materials_str;
|
||||
entry["cover"] = cover_path.string();
|
||||
entry["cover"] = into_u8(file_url_from_path(cover_path));
|
||||
entry["nozzle_selected"] = "";
|
||||
entry["sub_path"] = "";
|
||||
m_ProfileJson["model"].push_back(entry);
|
||||
@@ -1405,23 +1405,26 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
||||
// is served from the shipped profiles. Each is stamped by name and
|
||||
// version alone: a profile change requires a version bump, so those two
|
||||
// determine content wherever the vendor's copy sits.
|
||||
struct VendorSource { std::string name; boost::filesystem::path dir; std::string version; };
|
||||
std::vector<VendorSource> ordered;
|
||||
auto add_vendor = [&ordered](const std::string& name, const boost::filesystem::path& dir) {
|
||||
std::vector<PresetBundle::VendorSource> ordered;
|
||||
json stamps = json::array();
|
||||
auto add_vendor = [&ordered, &stamps](const std::string& name, const boost::filesystem::path& dir) {
|
||||
// The version a load from `dir` would serve: the profile's where one
|
||||
// exists (a cache is only served while it covers the profile beside
|
||||
// it), the cache's own stamp where the cache is the whole vendor.
|
||||
// A profile without a version (blacklist.json) carries no presets
|
||||
// and is passed over.
|
||||
const boost::filesystem::path profile = dir / (name + ".json");
|
||||
std::string version;
|
||||
if (boost::filesystem::exists(profile)) {
|
||||
const Semver v = get_version_from_json(profile.string());
|
||||
if (v.valid())
|
||||
ordered.push_back({name, dir, v.to_string()});
|
||||
if (! v.valid())
|
||||
return;
|
||||
version = v.to_string();
|
||||
} else {
|
||||
ordered.push_back({name, dir,
|
||||
VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name)});
|
||||
version = VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name);
|
||||
}
|
||||
ordered.push_back({name, dir});
|
||||
stamps.push_back({name, version});
|
||||
};
|
||||
const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||
if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end())
|
||||
@@ -1431,9 +1434,6 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
||||
add_vendor(name, dir);
|
||||
if (ordered.empty())
|
||||
return false;
|
||||
json stamps = json::array();
|
||||
for (const VendorSource& v : ordered)
|
||||
stamps.push_back({v.name, v.version});
|
||||
|
||||
// What this function derives is a pure function of that stamped set, so
|
||||
// the derived JSON is cached whole: a fresh cache makes an open one
|
||||
@@ -1461,26 +1461,18 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
||||
}
|
||||
|
||||
// Each vendor comes from its preset cache where one covers it, which is
|
||||
// what makes this worth doing instead of the scan below; loading into a
|
||||
// bundle per vendor keeps the install order the startup path has.
|
||||
PresetBundle bundle;
|
||||
auto load_vendor = [](PresetBundle& into, const std::string& vendor,
|
||||
const boost::filesystem::path& dir, const PresetBundle* base) {
|
||||
into.load_vendor_configs_from_json(dir.string(), vendor, PresetBundle::LoadSystem,
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, base);
|
||||
};
|
||||
for (const VendorSource& v : ordered) {
|
||||
if (*m_cancel_token)
|
||||
return false; // as in the scan below: a vendor without a cache is parsed, and that takes time
|
||||
if (v.name == filament_library) {
|
||||
load_vendor(bundle, v.name, v.dir, nullptr);
|
||||
} else {
|
||||
PresetBundle tmp;
|
||||
load_vendor(tmp, v.name, v.dir, &bundle);
|
||||
bundle.merge_presets(std::move(tmp));
|
||||
}
|
||||
}
|
||||
if (bundle.vendors.empty())
|
||||
// what makes this worth doing instead of the scan below.
|
||||
PresetBundle bundle;
|
||||
std::vector<std::string> failed;
|
||||
const std::string errors = bundle.load_vendors(ordered, ForwardCompatibilitySubstitutionRule::EnableSilent,
|
||||
/*allow_cache=*/true, m_cancel_token.get(), &failed).second;
|
||||
if (*m_cancel_token || bundle.vendors.empty())
|
||||
return false;
|
||||
if (! errors.empty())
|
||||
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: loading the vendors reported: " << errors;
|
||||
// A vendor that failed to load sends this open to the scan below, which lists
|
||||
// what it can read of every vendor.
|
||||
if (! failed.empty())
|
||||
return false;
|
||||
if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false))
|
||||
return false;
|
||||
@@ -1732,7 +1724,7 @@ int GuideFrame::LoadProfileFamily(std::string strVendor, std::string strFilePath
|
||||
cover_path = (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") / cover_file)
|
||||
.make_preferred();
|
||||
}
|
||||
OneModel["cover"] = cover_path.string();
|
||||
OneModel["cover"] = into_u8(file_url_from_path(cover_path));
|
||||
|
||||
OneModel["nozzle_selected"] = "";
|
||||
|
||||
|
||||
@@ -599,7 +599,7 @@ void ZUserLogin::OnScriptResponseMessage(wxCommandEvent &WXUNUSED(evt))
|
||||
|
||||
bool ZUserLogin::ShowErrorPage()
|
||||
{
|
||||
wxString ErrortUrl = from_u8((boost::filesystem::path(resources_dir()) / "web\\login\\error.html").make_preferred().string());
|
||||
wxString ErrortUrl = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/login/error.html");
|
||||
load_url(ErrortUrl);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -36,10 +36,10 @@ namespace GUI {
|
||||
WebViewPanel::WebViewPanel(wxWindow *parent)
|
||||
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
|
||||
{
|
||||
wxString url = wxString::Format("file://%s/web/homepage/index.html", from_u8(resources_dir()));
|
||||
wxString url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/homepage/index.html");
|
||||
wxString strlang = wxGetApp().current_language_code_safe();
|
||||
if (strlang != "")
|
||||
url = wxString::Format("file://%s/web/homepage/index.html?lang=%s", from_u8(resources_dir()), strlang);
|
||||
url += "?lang=" + strlang;
|
||||
|
||||
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "MultiNozzleSync.hpp"
|
||||
|
||||
#include "../GUI.hpp"
|
||||
#include "../GUI_App.hpp"
|
||||
#include "../I18N.hpp"
|
||||
#include "../Plater.hpp"
|
||||
@@ -21,8 +22,6 @@
|
||||
#include <set>
|
||||
|
||||
#include <wx/choice.h>
|
||||
#include <wx/filename.h>
|
||||
#include <wx/filesys.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/stattext.h>
|
||||
|
||||
@@ -70,8 +69,8 @@ int getExtruderNozzleCountTotal(PresetBundle *preset_bundle, int extruder_id)
|
||||
|
||||
// ---- ManualNozzleCountDialog ----------------------------------------------------------------------------------
|
||||
|
||||
ManualNozzleCountDialog::ManualNozzleCountDialog(
|
||||
wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int max_nozzle_count, bool force_no_zero)
|
||||
ManualNozzleCountDialog::ManualNozzleCountDialog(wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int e3d_count,
|
||||
int max_nozzle_count, bool force_no_zero, const std::set<NozzleVolumeType> &supported_types)
|
||||
: DPIDialog(parent, wxID_ANY, _L("Set nozzle count"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
|
||||
{
|
||||
SetBackgroundColour(*wxWHITE);
|
||||
@@ -93,43 +92,39 @@ ManualNozzleCountDialog::ManualNozzleCountDialog(
|
||||
for (int i = 0; i <= max_nozzle_count; ++i)
|
||||
nozzle_choices.Add(wxString::Format("%d", i));
|
||||
|
||||
// A Hybrid extruder mixes Standard and High Flow nozzles, so it gets both count choices; the concrete
|
||||
// types get exactly one.
|
||||
if (volume_type == nvtStandard || volume_type == nvtHybrid) {
|
||||
wxBoxSizer *standard_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto standard_label = new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(nvtStandard)), wxDefaultPosition, wxSize(FromDIP(100), -1));
|
||||
standard_label->SetFont(Label::Body_14);
|
||||
standard_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
|
||||
standard_sizer->Add(standard_label, 0, wxALIGN_CENTER_VERTICAL);
|
||||
m_standard_choice = new ComboBox(content, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1), 0, nullptr, wxCB_READONLY);
|
||||
std::vector<wxString>::iterator iter;
|
||||
for (iter = nozzle_choices.begin(); iter != nozzle_choices.end(); iter++)
|
||||
m_standard_choice->Append(*iter);
|
||||
m_standard_choice->SetSelection(standard_count);
|
||||
standard_sizer->Add(m_standard_choice, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5));
|
||||
choice_sizer->Add(standard_sizer, 0, wxTOP | wxRIGHT, FromDIP(15));
|
||||
}
|
||||
if (volume_type == nvtHighFlow || volume_type == nvtHybrid) {
|
||||
wxBoxSizer *highflow_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto highflow_label = new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(nvtHighFlow)), wxDefaultPosition, wxSize(FromDIP(100), -1));
|
||||
highflow_label->SetFont(Label::Body_14);
|
||||
highflow_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
|
||||
highflow_sizer->Add(highflow_label, 0, wxALIGN_CENTER_VERTICAL);
|
||||
m_highflow_choice = new ComboBox(content, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1), 0, nullptr, wxCB_READONLY);
|
||||
std::vector<wxString>::iterator iter;
|
||||
for (iter = nozzle_choices.begin(); iter != nozzle_choices.end(); iter++)
|
||||
m_highflow_choice->Append(*iter);
|
||||
m_highflow_choice->SetSelection(highflow_count);
|
||||
highflow_sizer->Add(m_highflow_choice, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5));
|
||||
choice_sizer->Add(highflow_sizer, 0, wxTOP | wxRIGHT, FromDIP(15));
|
||||
}
|
||||
// A Hybrid extruder mixes several nozzle volume types, so it gets a count choice for each type the
|
||||
// extruder actually has; a concrete type gets exactly its own, which it supports by definition.
|
||||
auto shows = [volume_type, &supported_types](NozzleVolumeType type) {
|
||||
return volume_type == type || (volume_type == nvtHybrid && (supported_types.empty() || supported_types.count(type) > 0));
|
||||
};
|
||||
auto add_count_choice = [this, content, choice_sizer, &nozzle_choices](NozzleVolumeType type, int count) {
|
||||
wxBoxSizer *row_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto label = new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(type)), wxDefaultPosition, wxSize(FromDIP(100), -1));
|
||||
label->SetFont(Label::Body_14);
|
||||
label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
|
||||
row_sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL);
|
||||
auto choice = new ComboBox(content, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1), 0, nullptr, wxCB_READONLY);
|
||||
for (const wxString &item : nozzle_choices)
|
||||
choice->Append(item);
|
||||
choice->SetSelection(count);
|
||||
row_sizer->Add(choice, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5));
|
||||
choice_sizer->Add(row_sizer, 0, wxTOP | wxRIGHT, FromDIP(15));
|
||||
return choice;
|
||||
};
|
||||
if (shows(nvtStandard))
|
||||
m_standard_choice = add_count_choice(nvtStandard, standard_count);
|
||||
if (shows(nvtHighFlow))
|
||||
m_highflow_choice = add_count_choice(nvtHighFlow, highflow_count);
|
||||
if (shows(nvtE3DHighFlow))
|
||||
m_e3d_choice = add_count_choice(nvtE3DHighFlow, e3d_count);
|
||||
|
||||
m_error_label = new wxStaticText(this, wxID_ANY, "");
|
||||
m_error_label->SetForegroundColour(wxColour("#E14747"));
|
||||
m_error_label->Hide();
|
||||
|
||||
auto update_nozzle_error = [this, force_no_zero, content, max_nozzle_count](int standard_count, int highflow_count) {
|
||||
const int total_count = standard_count + highflow_count;
|
||||
auto update_nozzle_error = [this, force_no_zero, content, max_nozzle_count](wxCommandEvent &e) {
|
||||
// The Hybrid count is the sum over every choice on screen, whatever the extruder's own type.
|
||||
const int total_count = GetNozzleCount(nvtHybrid);
|
||||
if (0 < total_count && total_count <= max_nozzle_count && m_error_label->IsShown()) {
|
||||
m_error_label->Hide();
|
||||
m_confirm_btn->Enable();
|
||||
@@ -145,18 +140,12 @@ ManualNozzleCountDialog::ManualNozzleCountDialog(
|
||||
Layout();
|
||||
Fit();
|
||||
}
|
||||
e.Skip();
|
||||
};
|
||||
|
||||
if (m_standard_choice)
|
||||
m_standard_choice->Bind(wxEVT_COMBOBOX, [this, update_nozzle_error](wxCommandEvent &e) {
|
||||
update_nozzle_error(m_standard_choice->GetSelection(), m_highflow_choice ? m_highflow_choice->GetSelection() : 0);
|
||||
e.Skip();
|
||||
});
|
||||
if (m_highflow_choice)
|
||||
m_highflow_choice->Bind(wxEVT_COMBOBOX, [this, update_nozzle_error](wxCommandEvent &e) {
|
||||
update_nozzle_error(m_standard_choice ? m_standard_choice->GetSelection() : 0, m_highflow_choice->GetSelection());
|
||||
e.Skip();
|
||||
});
|
||||
for (ComboBox *choice : {m_standard_choice, m_highflow_choice, m_e3d_choice})
|
||||
if (choice)
|
||||
choice->Bind(wxEVT_COMBOBOX, update_nozzle_error);
|
||||
|
||||
content_sizer->Add(nozzle_icon, 0, wxALL | wxALIGN_CENTER_VERTICAL, FromDIP(15));
|
||||
content_sizer->Add(choice_sizer, 0, wxALIGN_CENTRE_VERTICAL);
|
||||
@@ -181,9 +170,10 @@ int ManualNozzleCountDialog::GetNozzleCount(NozzleVolumeType volume_type) const
|
||||
return m_standard_choice ? m_standard_choice->GetSelection() : 0;
|
||||
if (volume_type == nvtHighFlow)
|
||||
return m_highflow_choice ? m_highflow_choice->GetSelection() : 0;
|
||||
if (volume_type == nvtE3DHighFlow)
|
||||
return m_e3d_choice ? m_e3d_choice->GetSelection() : 0;
|
||||
if (volume_type == nvtHybrid)
|
||||
return (m_standard_choice ? m_standard_choice->GetSelection() : 0) +
|
||||
(m_highflow_choice ? m_highflow_choice->GetSelection() : 0);
|
||||
return GetNozzleCount(nvtStandard) + GetNozzleCount(nvtHighFlow) + GetNozzleCount(nvtE3DHighFlow);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -318,6 +308,7 @@ void manuallySetNozzleCount(int extruder_id)
|
||||
const NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volume_type_opt->values[extruder_id]);
|
||||
const int standard_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtStandard);
|
||||
const int highflow_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtHighFlow);
|
||||
const int e3d_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtE3DHighFlow);
|
||||
|
||||
// Require at least one nozzle for a Hybrid extruder (an empty mix is meaningless) and when the other
|
||||
// extruder currently has none.
|
||||
@@ -325,11 +316,19 @@ void manuallySetNozzleCount(int extruder_id)
|
||||
if (nozzle_volume_type_opt->values.size() > 1)
|
||||
force_no_zero |= getExtruderNozzleCountTotal(preset_bundle, 1 - extruder_id) == 0;
|
||||
|
||||
ManualNozzleCountDialog dialog(wxGetApp().plater(), volume_type, standard_count, highflow_count, max_nozzle_count->values[extruder_id], force_no_zero);
|
||||
// The printer profile's variant list is the authority on which volume types an extruder provides. An
|
||||
// empty set means the profile could not be read, and the dialog then skips filtering instead of hiding
|
||||
// everything.
|
||||
const std::set<NozzleVolumeType> supported_types =
|
||||
get_extruder_supported_nozzle_volume_types(preset_bundle->printers.get_edited_preset().config, extruder_id);
|
||||
|
||||
ManualNozzleCountDialog dialog(wxGetApp().plater(), volume_type, standard_count, highflow_count, e3d_count,
|
||||
max_nozzle_count->values[extruder_id], force_no_zero, supported_types);
|
||||
if (dialog.ShowModal() == wxID_OK) {
|
||||
if (volume_type == nvtHybrid) {
|
||||
setExtruderNozzleCount(preset_bundle, extruder_id, nvtStandard, dialog.GetNozzleCount(nvtStandard), true);
|
||||
setExtruderNozzleCount(preset_bundle, extruder_id, nvtHighFlow, dialog.GetNozzleCount(nvtHighFlow), false);
|
||||
setExtruderNozzleCount(preset_bundle, extruder_id, nvtE3DHighFlow, dialog.GetNozzleCount(nvtE3DHighFlow), false);
|
||||
} else {
|
||||
setExtruderNozzleCount(preset_bundle, extruder_id, volume_type, dialog.GetNozzleCount(volume_type), true);
|
||||
}
|
||||
@@ -641,8 +640,7 @@ NozzleListTable::NozzleListTable(wxWindow* parent) : wxPanel(parent,wxID_ANY,wxD
|
||||
m_web_view->AddScriptMessageHandler("nozzleListTable");
|
||||
m_web_view->EnableContextMenu(false);
|
||||
fs::path filepath = fs::path(resources_dir()) / "web/flush/NozzleListTable.html";
|
||||
wxFileName fn(wxString::FromUTF8(filepath.string()));
|
||||
wxString url = wxFileSystem::FileNameToURL(fn);
|
||||
wxString url = file_url_from_path(filepath);
|
||||
m_web_view->LoadURL(url);
|
||||
|
||||
auto sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
@@ -64,7 +65,9 @@ struct NozzleOption
|
||||
class ManualNozzleCountDialog : public DPIDialog
|
||||
{
|
||||
public:
|
||||
ManualNozzleCountDialog(wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int max_nozzle_count, bool force_no_zero);
|
||||
// supported_types lists the volume types the extruder physically provides; an empty set means "unknown", in which case no filtering is applied.
|
||||
ManualNozzleCountDialog(wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int e3d_count, int max_nozzle_count, bool force_no_zero,
|
||||
const std::set<NozzleVolumeType> &supported_types);
|
||||
~ManualNozzleCountDialog() override = default;
|
||||
void on_dpi_changed(const wxRect &suggested_rect) override {}
|
||||
int GetNozzleCount(NozzleVolumeType volume_type) const;
|
||||
@@ -72,6 +75,7 @@ public:
|
||||
private:
|
||||
ComboBox *m_standard_choice{nullptr};
|
||||
ComboBox *m_highflow_choice{nullptr};
|
||||
ComboBox *m_e3d_choice{nullptr};
|
||||
Button *m_confirm_btn{nullptr};
|
||||
wxStaticText *m_error_label{nullptr};
|
||||
};
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "StaticBox.hpp"
|
||||
|
||||
#include "../wxExtensions.hpp"
|
||||
|
||||
#include <wx/settings.h>
|
||||
#include "../GUI_App.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "../Utils/MacDarkMode.hpp"
|
||||
@@ -620,19 +622,34 @@ MultiSwitchButton::MultiSwitchButton(wxWindow *parent, wxWindowID id, const wxPo
|
||||
std::make_pair(0x6B6B6B, (int) StateColor::NotChecked),
|
||||
std::make_pair(0xFFFFFE, (int) StateColor::Normal)))
|
||||
, m_button_radius(10.0)
|
||||
, m_button_padding(10, 6)
|
||||
, m_button_padding(FromDIP(wxSize(11, 3)))
|
||||
{
|
||||
SetCornerRadius(m_button_radius);
|
||||
SetBorderWidth(0);
|
||||
|
||||
sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto *hsizer = new wxBoxSizer(wxVERTICAL);
|
||||
hsizer->Add(sizer, 1, wxEXPAND);
|
||||
SetSizer(hsizer);
|
||||
SetMinSize(wxSize(-1, 20));
|
||||
// Orca: a switch can hold more buttons than the layout has room for (a toolchanger lists one per
|
||||
// tool), so they live in a scrolled area: the caller caps the switch at its natural width and
|
||||
// this scrolls horizontally instead of clipping the last buttons.
|
||||
m_scroll = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxBORDER_NONE);
|
||||
m_scroll->SetBackgroundColour(GetBackgroundColour());
|
||||
// The buttons are a single row, so only the horizontal bar may ever appear: a vertical one would
|
||||
// eat into the row's height.
|
||||
m_scroll->ShowScrollbars(wxSHOW_SB_DEFAULT, wxSHOW_SB_NEVER);
|
||||
m_scroll->EnableScrolling(true, false);
|
||||
m_scroll->SetScrollRate(FromDIP(10), 0);
|
||||
|
||||
sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
m_scroll->SetSizer(sizer);
|
||||
|
||||
auto *hsizer = new wxBoxSizer(wxVERTICAL);
|
||||
hsizer->Add(m_scroll, 1, wxEXPAND);
|
||||
SetSizer(hsizer);
|
||||
SetMinSize(wxSize(-1, options_height()));
|
||||
|
||||
Bind(wxEVT_SIZE, &MultiSwitchButton::on_size, this);
|
||||
Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MultiSwitchButton::button_clicked, this);
|
||||
SetFont(Label::Body_12);
|
||||
// The tags name a tool and its volume type only, so they stay compact.
|
||||
SetFont(Label::Body_10);
|
||||
}
|
||||
|
||||
MultiSwitchButton::~MultiSwitchButton()
|
||||
@@ -640,10 +657,88 @@ MultiSwitchButton::~MultiSwitchButton()
|
||||
DeleteAllOptions();
|
||||
}
|
||||
|
||||
int MultiSwitchButton::options_height() const
|
||||
{
|
||||
// With no button to measure yet, keep the placeholder height the switch starts with.
|
||||
return btns.empty() ? FromDIP(20) : btns.front()->GetMinSize().y;
|
||||
}
|
||||
|
||||
void MultiSwitchButton::update_scroll_range()
|
||||
{
|
||||
// The scrollbar range is measured against the virtual size, so it has to follow the buttons
|
||||
// whenever their labels or count change.
|
||||
m_scroll->InvalidateBestSize();
|
||||
m_scroll->FitInside();
|
||||
|
||||
// A scrolled window reports its min size plus a scrollbar as its best size, never the width of
|
||||
// the buttons it holds, so the layout has to be given that width explicitly. It is also the
|
||||
// widest this switch wants to be: a row with less room squeezes it below this and it scrolls.
|
||||
const wxSize content = sizer->CalcMin();
|
||||
SetMinSize(wxSize(content.x, options_height() + scrollbar_height(content.x)));
|
||||
SetMaxSize(m_fit_to_options ? wxSize(content.x, -1) : wxDefaultSize);
|
||||
InvalidateBestSize();
|
||||
}
|
||||
|
||||
int MultiSwitchButton::scrollbar_height(int options_width) const
|
||||
{
|
||||
// The bar is drawn inside the switch, so while the buttons need more width than the row gave us
|
||||
// the switch has to be taller by the bar's height, or the bar sits on top of the buttons.
|
||||
const int width = GetClientSize().x;
|
||||
if (width <= 0 || options_width <= width)
|
||||
return 0;
|
||||
|
||||
const int bar = wxSystemSettings::GetMetric(wxSYS_HSCROLL_Y, this);
|
||||
return bar > 0 ? bar : 0;
|
||||
}
|
||||
|
||||
void MultiSwitchButton::on_size(wxSizeEvent &evt)
|
||||
{
|
||||
evt.Skip();
|
||||
|
||||
// The row resized, so the buttons may now overflow it (or no longer fit in it) and the room the
|
||||
// bar needs changed with that. Width does not depend on height, so this settles in one pass.
|
||||
const int height = options_height() + scrollbar_height(sizer->CalcMin().x);
|
||||
if (GetMinSize().y != height) {
|
||||
SetMinSize(wxSize(GetMinSize().x, height));
|
||||
// The switch sits in a row of this tab, and the tab in a panel that shares its height
|
||||
// with the page view, so both have to lay out again for the taller row to get its room.
|
||||
if (wxWindow *tab = GetParent()) {
|
||||
tab->Layout();
|
||||
if (wxWindow *panel = tab->GetParent())
|
||||
panel->Layout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MultiSwitchButton::scroll_option_into_view(Button *btn)
|
||||
{
|
||||
const int width = m_scroll->GetClientSize().x;
|
||||
const int view_x = m_scroll->GetViewStartPixels().x;
|
||||
if (width <= 0) // not laid out yet: there is no view to scroll
|
||||
return;
|
||||
|
||||
const wxRect rect = btn->GetRect();
|
||||
const int right = rect.GetRight() + 1;
|
||||
int target = view_x;
|
||||
if (rect.x < view_x)
|
||||
target = rect.x;
|
||||
else if (right > view_x + width)
|
||||
target = right - width;
|
||||
|
||||
if (target == view_x)
|
||||
return;
|
||||
|
||||
// Scroll() counts scroll units; round up so the whole button ends up inside the view rather
|
||||
// than a few pixels short of it.
|
||||
int step = 1;
|
||||
m_scroll->GetScrollPixelsPerUnit(&step, nullptr);
|
||||
m_scroll->Scroll((target + step - 1) / step, -1);
|
||||
}
|
||||
|
||||
int MultiSwitchButton::AppendOption(const wxString &option, void *clientData)
|
||||
{
|
||||
Button *btn = new Button();
|
||||
btn->Create(this, option, "", wxBORDER_NONE);
|
||||
btn->Create(m_scroll, option, "", wxBORDER_NONE);
|
||||
btn->SetFont(GetFont());
|
||||
btn->SetBackgroundColor(m_bg_color);
|
||||
btn->SetTextColor(m_text_color);
|
||||
@@ -654,9 +749,6 @@ int MultiSwitchButton::AppendOption(const wxString &option, void *clientData)
|
||||
btns.push_back(btn);
|
||||
sizer->Add(btn, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
wxSize text_size = btn->GetTextExtent(option);
|
||||
btn->SetMinSize(wxSize(text_size.x + m_button_padding.x * 2 + 6, -1));
|
||||
|
||||
return int(btns.size()) - 1;
|
||||
}
|
||||
|
||||
@@ -666,6 +758,7 @@ void MultiSwitchButton::SetOptions(const std::vector<wxString> &options)
|
||||
for (const auto &option : options)
|
||||
AppendOption(option);
|
||||
|
||||
update_scroll_range();
|
||||
Layout();
|
||||
Refresh();
|
||||
}
|
||||
@@ -700,6 +793,8 @@ void MultiSwitchButton::SetSelection(int index)
|
||||
sel = index;
|
||||
update_button_styles();
|
||||
send_selection_event();
|
||||
// The selected button may be scrolled out of sight, e.g. when the tab restores the active tool.
|
||||
scroll_option_into_view(btns[sel]);
|
||||
Refresh();
|
||||
}
|
||||
|
||||
@@ -718,6 +813,7 @@ void MultiSwitchButton::SetOptionText(unsigned int index, const wxString &text)
|
||||
if (index >= btns.size())
|
||||
return;
|
||||
btns[index]->SetLabel(text);
|
||||
update_scroll_range();
|
||||
}
|
||||
|
||||
void *MultiSwitchButton::GetOptionData(unsigned int index) const
|
||||
@@ -769,6 +865,7 @@ void MultiSwitchButton::SetButtonPadding(const wxSize &padding)
|
||||
m_button_padding = padding;
|
||||
for (auto *btn : btns)
|
||||
btn->SetPaddingSize(padding);
|
||||
update_scroll_range();
|
||||
Layout();
|
||||
Refresh();
|
||||
}
|
||||
@@ -777,6 +874,8 @@ void MultiSwitchButton::Rescale()
|
||||
{
|
||||
for (auto *btn : btns)
|
||||
btn->Rescale();
|
||||
// Rescaling can change how the labels measure, and the scrollbar range follows the buttons.
|
||||
update_scroll_range();
|
||||
}
|
||||
|
||||
void MultiSwitchButton::button_clicked(wxCommandEvent &event)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <vector>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/scrolwin.h>
|
||||
#include <wx/tglbtn.h>
|
||||
#include "Button.hpp"
|
||||
|
||||
@@ -164,6 +165,10 @@ public:
|
||||
void SetButtonCornerRadius(double radius);
|
||||
void SetButtonPadding(const wxSize &padding);
|
||||
|
||||
// Keep the switch exactly as wide as the buttons need instead of letting the layout stretch it.
|
||||
// A layout with less room than that still squeezes it, and it scrolls its buttons then.
|
||||
void SetFitToOptions(bool fit = true) { m_fit_to_options = fit; update_scroll_range(); }
|
||||
|
||||
void Rescale();
|
||||
|
||||
protected:
|
||||
@@ -173,9 +178,20 @@ protected:
|
||||
bool send_selection_event();
|
||||
|
||||
private:
|
||||
// Height of the single button row, measured from the buttons themselves.
|
||||
int options_height() const;
|
||||
void update_scroll_range();
|
||||
int scrollbar_height(int options_width) const;
|
||||
void scroll_option_into_view(Button *btn);
|
||||
void on_size(wxSizeEvent &evt);
|
||||
|
||||
std::vector<Button *> btns;
|
||||
wxBoxSizer *sizer = nullptr;
|
||||
int sel = -1;
|
||||
// The buttons are laid out inside this scrolled area so that a switch holding more options than
|
||||
// the layout has room for scrolls instead of clipping its tail.
|
||||
wxScrolledWindow *m_scroll = nullptr;
|
||||
wxBoxSizer *sizer = nullptr;
|
||||
bool m_fit_to_options = false;
|
||||
int sel = -1;
|
||||
|
||||
StateColor m_bg_color;
|
||||
StateColor m_text_color;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user