Merge branch 'main' into feat/printer-agent-infra

This commit is contained in:
Ian Chua
2026-09-30 19:29:07 +08:00
committed by GitHub
2957 changed files with 36243 additions and 14894 deletions
+93 -11
View File
@@ -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, &current_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;
+273 -96
View File
@@ -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 &center)
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
+4
View File
@@ -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);
}
+1
View File
@@ -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()) {
+3
View File
@@ -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
View File
@@ -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();
+94 -122
View File
@@ -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 &param
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 &para
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 &para
{
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;
}
+1 -1
View File
@@ -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;
+68 -12
View File
@@ -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
+21 -8
View File
@@ -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));
+20 -11
View File
@@ -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;
+50 -7
View File
@@ -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()) {
+4 -5
View File
@@ -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);
+2 -1
View File
@@ -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
View File
@@ -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 << "[";
+2
View File
@@ -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();
+26 -33
View File
@@ -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
}
+7 -11
View File
@@ -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
+146
View File
@@ -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_
+160 -42
View File
@@ -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 &current = 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,
+12 -3
View File
@@ -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);
+16
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+65
View File
@@ -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
+34 -11
View File
@@ -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());
+8
View File
@@ -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(); }
+10 -1
View File
@@ -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;
}
}
}
+3 -1
View File
@@ -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
View File
@@ -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;
}
+60 -3
View File
@@ -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)
File diff suppressed because it is too large Load Diff
+182 -29
View File
@@ -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).
+1 -1
View File
@@ -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
View File
@@ -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;
}
+6
View File
@@ -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;
+54 -16
View File
@@ -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;
+2 -1
View File
@@ -62,7 +62,8 @@ public:
SlicingReplaceInitEmptyLayers,
SlicingNeedSupportOn,
SlicingEmptyGcodeLayers,
SlicingGcodeOverlap
SlicingGcodeOverlap,
SlicingPreciseSeamWarning
};
typedef size_t TimeStamp;
+89 -6
View File
@@ -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()
+23 -3
View File
@@ -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);
+29 -23
View File
@@ -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());
+68 -43
View File
@@ -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);
+14 -2
View File
@@ -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
View File
@@ -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 {
+63
View File
@@ -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);
+2
View File
@@ -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
View File
@@ -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
+2 -7
View File
@@ -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.
+30
View File
@@ -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;
+7
View File
@@ -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();
+1 -1
View File
@@ -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)
+10 -6
View File
@@ -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()];
+32 -33
View File
@@ -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);
+8 -6
View File
@@ -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);
+2 -1
View File
@@ -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 = {
+6 -5
View File
@@ -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:
+8 -1
View File
@@ -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()) {
+51 -32
View File
@@ -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*/)
+2 -1
View File
@@ -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();
}
}
+7 -1
View File
@@ -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);
+5 -2
View File
@@ -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,
+32
View File
@@ -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);
}
} }
+6
View File
@@ -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
+26 -5
View File
@@ -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) {
+170 -9
View File
@@ -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;
+1
View File
@@ -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);
+131 -10
View File
@@ -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) {
+7 -1
View File
@@ -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);
+4 -2
View File
@@ -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());
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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
View File
@@ -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)
+9
View File
@@ -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;
+1 -4
View File
@@ -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();
}
+4 -1
View File
@@ -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());
+2 -1
View File
@@ -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 ? (
+1 -1
View File
@@ -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
View File
@@ -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});
+4 -1
View File
@@ -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);
+1
View File
@@ -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;
}
+1 -3
View File
@@ -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));
+4 -6
View File
@@ -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") {
+1 -1
View File
@@ -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());
+3
View File
@@ -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";
+1 -1
View File
@@ -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;
}
}
+4
View File
@@ -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
View File
@@ -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);
+5 -4
View File
@@ -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")) {
+1 -3
View File
@@ -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);
+34 -42
View File
@@ -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"] = "";
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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);
+49 -51
View File
@@ -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);
+5 -1
View File
@@ -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};
};
+110 -11
View File
@@ -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)
+18 -2
View File
@@ -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