Merge branch 'main' into pr/grant0013/13752

This commit is contained in:
SoftFever
2026-06-06 17:14:33 +08:00
947 changed files with 92603 additions and 14553 deletions
+8
View File
@@ -1357,6 +1357,10 @@ int CLI::run(int argc, char **argv)
else {
set_logging_level(2);
}
const ConfigOptionString* opt_logfile = m_config.opt<ConfigOptionString>("logfile");
if (opt_logfile) {
set_logging_file(opt_logfile->value);
}
global_begin_time = (long long)Slic3r::Utils::get_current_time_utc();
BOOST_LOG_TRIVIAL(warning) << boost::format("cli mode, Current OrcaSlicer Version %1%")%SoftFever_VERSION;
@@ -1606,6 +1610,10 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << boost::format("old 3mf version %1%, need to set enable_wrapping_detection to false")%file_version.to_string();
}
// ORCA: legacy feature-filament default migration (1 -> 0) is now handled
// uniformly in PrintConfigDef::handle_legacy() via the old->new key rename
// (wall_filament -> wall_filament_id, etc.), which covers presets too.
if (normative_check) {
ConfigOptionStrings* postprocess_scripts = config.option<ConfigOptionStrings>("post_process");
if (postprocess_scripts) {
@@ -266,10 +266,44 @@ std::vector<WaveSeed> wave_seeds(
//(front.z() < 0 && back.z() < 0));
// Hope that at least one end of an open polyline is clipped by the boundary, thus an intersection point is created.
(front.z() < 0 || back.z() < 0));
// However, with complex geometry, both endpoints may coincide with existing polygon
// vertices (z >= 0), which is handled below.
if (front != back && front.z() >= 0 && back.z() >= 0) {
// Very rare case when both endpoints intersect boundary ExPolygons in existing points.
// So the ZFillFunction callback hasn't been called.
// Both endpoints coincide with existing polygon vertices, so the
// ZFillFunction callback was never called. With complex geometry
// this is common because source and boundary contours share many
// vertices. Determine src_id / boundary_id from Z coordinates
// (and fall back to an AABB-tree point-in-polygon test when a
// boundary ID is not directly available).
coord_t src_z = -1, boundary_z = -1;
// Scan all path points for the information we need.
for (const ClipperLib_Z::IntPoint &point : path) {
if (point.z() >= idx_boundary_end && point.z() < idx_src_end && src_z < 0)
src_z = point.z();
else if (point.z() >= idx_boundary_begin && point.z() < idx_boundary_end && boundary_z < 0)
boundary_z = point.z();
if (src_z >= 0 && boundary_z >= 0)
break;
}
if (src_z >= 0) {
uint32_t src_id = uint32_t(src_z - idx_boundary_end);
if (boundary_z >= 0) {
out.push_back({ src_id, uint32_t(boundary_z - 1), ClipperZUtils::from_zpath(path) });
} else {
// Source ID known but boundary unknown – use AABB tree.
if (aabb_tree.empty())
aabb_tree = build_aabb_tree_over_expolygons(boundary);
int boundary_id = sample_in_expolygons(aabb_tree, boundary, Point(front.x(), front.y()));
if (boundary_id >= 0)
out.push_back({ src_id, uint32_t(boundary_id), ClipperZUtils::from_zpath(path) });
}
++ iseed;
continue;
}
// Unable to determine source ID – drop the segment.
continue;
} else
if (front == back && (front.z() < idx_boundary_end)) {
+26
View File
@@ -93,6 +93,11 @@ bool AppConfig::get_stealth_mode()
return get_bool("stealth_mode");
}
bool AppConfig::get_hide_login_side_panel()
{
return get_bool("hide_login_side_panel");
}
void AppConfig::reset()
{
m_storage.clear();
@@ -259,6 +264,21 @@ void AppConfig::set_defaults()
if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty())
set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false);
if (get(SETTING_OPENGL_REALISTIC_MODE).empty())
set_bool(SETTING_OPENGL_REALISTIC_MODE, false);
if (get(SETTING_OPENGL_REALISTIC_PHONG).empty())
set_bool(SETTING_OPENGL_REALISTIC_PHONG, true);
if (get(SETTING_OPENGL_SHADING_MODEL).empty())
set(SETTING_OPENGL_SHADING_MODEL, "gouraud");
if (get(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS).empty())
set_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS, false);
if (get(SETTING_OPENGL_PHONG_SSAO).empty())
set_bool(SETTING_OPENGL_PHONG_SSAO, false);
if (get("export_sources_full_pathnames").empty())
set_bool("export_sources_full_pathnames", false);
@@ -321,6 +341,9 @@ void AppConfig::set_defaults()
if (get("developer_mode").empty())
set_bool("developer_mode", false);
if (get("show_unsupported_presets").empty())
set_bool("show_unsupported_presets", false);
if (get("enable_ssl_for_mqtt").empty())
set_bool("enable_ssl_for_mqtt", true);
@@ -347,6 +370,9 @@ void AppConfig::set_defaults()
if (get("stealth_mode").empty()) {
set_bool("stealth_mode", false);
}
if (get("hide_login_side_panel").empty()) {
set_bool("hide_login_side_panel", false);
}
if (get("allow_abnormal_storage").empty()) {
set_bool("allow_abnormal_storage", false);
}
+6
View File
@@ -34,6 +34,11 @@ using namespace nlohmann;
#define SETTING_OPENGL_FXAA_ENABLED "opengl_fxaa_enabled"
#define SETTING_OPENGL_FPS_CAP "opengl_fps_cap"
#define SETTING_OPENGL_SHOW_FPS_OVERLAY "opengl_show_fps_overlay"
#define SETTING_OPENGL_REALISTIC_MODE "opengl_realistic_mode"
#define SETTING_OPENGL_REALISTIC_PHONG "opengl_realistic_phong"
#define SETTING_OPENGL_SHADING_MODEL "opengl_shading_model"
#define SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS "opengl_phong_basic_plate_shadows"
#define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao"
#if defined(_WIN32) || defined(_WIN64)
#define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09"
@@ -85,6 +90,7 @@ public:
std::string get_language_code();
std::string get_hms_host();
bool get_stealth_mode();
bool get_hide_login_side_panel();
// Clear and reset to defaults.
void reset();
+62 -50
View File
@@ -256,17 +256,15 @@ struct SurfaceFillParams
// Index of this entry in a linear vector.
size_t idx = 0;
// infill speed settings
float sparse_infill_speed = 0;
float top_surface_speed = 0;
float solid_infill_speed = 0;
// Infill speed setting for the effective extrusion role.
float role_speed = 0;
// Params for lattice infill angles
float lateral_lattice_angle_1 = 0.f;
float lateral_lattice_angle_2 = 0.f;
float infill_lock_depth = 0;
float skin_infill_depth = 0;
bool symmetric_infill_y_axis = false;
float infill_lock_depth = 0;
float skin_infill_depth = 0;
bool symmetric_infill_y_axis = false;
// Params for Lateral honeycomb
float infill_overhang_angle = 60.f;
@@ -298,9 +296,7 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(flow.nozzle_diameter());
RETURN_COMPARE_NON_EQUAL_TYPED(unsigned, bridge);
RETURN_COMPARE_NON_EQUAL_TYPED(unsigned, extrusion_role);
RETURN_COMPARE_NON_EQUAL(sparse_infill_speed);
RETURN_COMPARE_NON_EQUAL(top_surface_speed);
RETURN_COMPARE_NON_EQUAL(solid_infill_speed);
RETURN_COMPARE_NON_EQUAL(role_speed);
RETURN_COMPARE_NON_EQUAL(lateral_lattice_angle_1);
RETURN_COMPARE_NON_EQUAL(lateral_lattice_angle_2);
RETURN_COMPARE_NON_EQUAL(symmetric_infill_y_axis);
@@ -312,30 +308,28 @@ struct SurfaceFillParams
}
bool operator==(const SurfaceFillParams &rhs) const {
return this->extruder == rhs.extruder &&
this->pattern == rhs.pattern &&
this->spacing == rhs.spacing &&
this->overlap == rhs.overlap &&
this->angle == rhs.angle &&
this->fixed_angle == rhs.fixed_angle &&
this->bridge == rhs.bridge &&
this->bridge_angle == rhs.bridge_angle &&
this->density == rhs.density &&
this->multiline == rhs.multiline &&
// this->dont_adjust == rhs.dont_adjust &&
this->anchor_length == rhs.anchor_length &&
this->anchor_length_max == rhs.anchor_length_max &&
this->flow == rhs.flow &&
this->extrusion_role == rhs.extrusion_role &&
this->sparse_infill_speed == rhs.sparse_infill_speed &&
this->top_surface_speed == rhs.top_surface_speed &&
this->solid_infill_speed == rhs.solid_infill_speed &&
this->lateral_lattice_angle_1 == rhs.lateral_lattice_angle_1 &&
this->lateral_lattice_angle_2 == rhs.lateral_lattice_angle_2 &&
this->infill_lock_depth == rhs.infill_lock_depth &&
this->skin_infill_depth == rhs.skin_infill_depth &&
this->infill_overhang_angle == rhs.infill_overhang_angle &&
this->gyroid_optimized == rhs.gyroid_optimized;
return this->extruder == rhs.extruder &&
this->pattern == rhs.pattern &&
this->spacing == rhs.spacing &&
this->overlap == rhs.overlap &&
this->angle == rhs.angle &&
this->fixed_angle == rhs.fixed_angle &&
this->bridge == rhs.bridge &&
this->bridge_angle == rhs.bridge_angle &&
this->density == rhs.density &&
this->multiline == rhs.multiline &&
// this->dont_adjust == rhs.dont_adjust &&
this->anchor_length == rhs.anchor_length &&
this->anchor_length_max == rhs.anchor_length_max &&
this->flow == rhs.flow &&
this->extrusion_role == rhs.extrusion_role &&
this->role_speed == rhs.role_speed &&
this->lateral_lattice_angle_1 == rhs.lateral_lattice_angle_1 &&
this->lateral_lattice_angle_2 == rhs.lateral_lattice_angle_2 &&
this->infill_lock_depth == rhs.infill_lock_depth &&
this->skin_infill_depth == rhs.skin_infill_depth &&
this->infill_overhang_angle == rhs.infill_overhang_angle &&
this->gyroid_optimized == rhs.gyroid_optimized;
}
};
@@ -922,6 +916,12 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.extrusion_role = erSolidInfill;
}
}
if (params.extrusion_role == erTopSolidInfill)
params.extruder = region_config.top_surface_filament_id;
else if (params.extrusion_role == erBottomSurface)
params.extruder = region_config.bottom_surface_filament_id;
else if (params.extrusion_role == erSolidInfill)
params.extruder = region_config.internal_solid_filament_id;
// Orca: apply fill multiline only for sparse infill
params.multiline = params.extrusion_role == erInternalInfill ? int(region_config.fill_multiline) : 1;
@@ -941,10 +941,13 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.fixed_angle = !region_config.solid_infill_rotate_template.value.empty();
}
params.bridge_angle = float(surface.bridge_angle);
// ORCA: Align infill angle to model
float align_offset = 0.f;
if (region_config.align_infill_direction_to_model) {
auto m = layer.object()->trafo().matrix();
params.angle += atan2((float) m(1, 0), (float) m(0, 0));
align_offset = atan2((float)m(1, 0), (float)m(0, 0));
params.angle += align_offset;
}
// Calculate the actual flow we'll be using for this infill.
@@ -954,15 +957,18 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
//Orca: enable thick bridge based on config
layerm.bridging_flow(extrusion_role, is_thick_bridge) :
layerm.flow(extrusion_role, (surface.thickness == -1) ? layer.height : surface.thickness);
// record speed params
if (!params.bridge) {
if (params.extrusion_role == erInternalInfill)
params.sparse_infill_speed = region_config.sparse_infill_speed;
else if (params.extrusion_role == erTopSolidInfill) {
params.top_surface_speed = region_config.top_surface_speed;
} else if (params.extrusion_role == erSolidInfill)
params.solid_infill_speed = region_config.internal_solid_infill_speed;
}
params.role_speed = 0;
if (params.extrusion_role == erBridgeInfill)
params.role_speed = region_config.bridge_speed;
else if (params.extrusion_role == erInternalBridgeInfill)
params.role_speed = region_config.get_abs_value("internal_bridge_speed");
else if (params.extrusion_role == erInternalInfill)
params.role_speed = region_config.sparse_infill_speed;
else if (params.extrusion_role == erTopSolidInfill)
params.role_speed = region_config.top_surface_speed;
else if (params.extrusion_role == erSolidInfill)
params.role_speed = region_config.internal_solid_infill_speed;
// Calculate flow spacing for infill pattern generation.
if (surface.is_solid() || is_bridge) {
params.spacing = params.flow.spacing();
@@ -1027,6 +1033,7 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
if (fill.region_id == size_t(-1)) {
fill.region_id = region_id;
fill.surface = surface;
fill.surface.bridge_angle = params->bridge_angle;
fill.expolygons.emplace_back(std::move(fill.surface.expolygon));
//BBS
fill.region_id_group.push_back(region_id);
@@ -1570,18 +1577,18 @@ void Layer::make_ironing()
((config.top_shell_layers > 0 || (this->object()->print()->config().spiral_mode && config.bottom_shell_layers > 1)) &&
(config.ironing_type == IroningType::TopSurfaces ||
(config.ironing_type == IroningType::TopmostOnly && layerm->layer()->upper_layer == nullptr))))) {
if (config.wall_filament == config.solid_infill_filament || config.wall_loops == 0) {
if (config.outer_wall_filament_id == config.top_surface_filament_id || config.wall_loops == 0) {
// Iron the whole face.
ironing_params.extruder = config.solid_infill_filament;
ironing_params.extruder = config.top_surface_filament_id;
} else {
// Iron just the infill.
ironing_params.extruder = config.solid_infill_filament;
ironing_params.extruder = config.top_surface_filament_id;
}
}
if (ironing_params.extruder != -1) {
//TODO just_infill is currently not used.
ironing_params.just_infill = false;
// Get filament-specific overrides if configured, otherwise use default values
// ORCA: Get filament-specific overrides if configured, otherwise use process values
size_t extruder_idx = ironing_params.extruder - 1;
ironing_params.line_spacing = (!config.filament_ironing_spacing.is_nil(extruder_idx)
? config.filament_ironing_spacing.get_at(extruder_idx)
@@ -1595,7 +1602,12 @@ void Layer::make_ironing()
ironing_params.speed = (!config.filament_ironing_speed.is_nil(extruder_idx)
? config.filament_ironing_speed.get_at(extruder_idx)
: config.ironing_speed);
ironing_params.angle = (config.ironing_angle_fixed ? 0 : calculate_infill_rotation_angle(this->object(), this->id(), config.solid_infill_direction.value, config.solid_infill_rotate_template.value)) + config.ironing_angle * M_PI / 180.;
double ironing_angle = (config.ironing_angle_fixed ? 0 : calculate_infill_rotation_angle(this->object(), this->id(), config.solid_infill_direction.value, config.solid_infill_rotate_template.value)) + config.ironing_angle * M_PI / 180.;
if (config.align_infill_direction_to_model) {
auto m = this->object()->trafo().matrix();
ironing_angle += atan2((double)m(1, 0), (double)m(0, 0));
}
ironing_params.angle = ironing_angle;
ironing_params.fixed_angle = config.ironing_angle_fixed || !config.solid_infill_rotate_template.value.empty();
ironing_params.pattern = config.ironing_pattern;
ironing_params.layerm = layerm;
+4 -4
View File
@@ -89,9 +89,9 @@ Generator::Generator(const PrintObject &print_object, const std::function<void()
m_supporting_radius = coord_t(m_infill_extrusion_width) * 100 * n_multiline / region_config.sparse_infill_density;
const double lightning_infill_overhang_angle = M_PI / 4; // 45 degrees
const double lightning_infill_prune_angle = M_PI / 4; // 45 degrees
const double lightning_infill_straightening_angle = M_PI / 4; // 45 degrees
const double lightning_infill_overhang_angle = region_config.lightning_overhang_angle.value * M_PI / 180.0;
const double lightning_infill_prune_angle = region_config.lightning_prune_angle.value * M_PI / 180.0;
const double lightning_infill_straightening_angle = region_config.lightning_straightening_angle.value * M_PI / 180.0;
m_wall_supporting_radius = coord_t(layer_thickness * std::tan(lightning_infill_overhang_angle));
m_prune_length = coord_t(layer_thickness * std::tan(lightning_infill_prune_angle));
m_straightening_max_distance = coord_t(layer_thickness * std::tan(lightning_infill_straightening_angle));
@@ -128,7 +128,7 @@ Generator::Generator(PrintObject* m_object, std::vector<Polygons>& contours, std
//TODO: decide whether enable density controller in advanced options or not
density = std::max(0.15f, density);
m_supporting_radius = coord_t(m_infill_extrusion_width) / density;
// Keep support-lightning behavior fixed and independent of user print-region angles.
const double lightning_infill_overhang_angle = M_PI / 4; // 45 degrees
const double lightning_infill_prune_angle = M_PI / 4; // 45 degrees
const double lightning_infill_straightening_angle = M_PI / 4; // 45 degrees
+24 -6
View File
@@ -49,6 +49,8 @@ static inline FlowRole opt_key_to_flow_role(const std::string &opt_key)
return frInfill;
else if (opt_key == "internal_solid_infill_line_width")
return frSolidInfill;
else if (opt_key == "bridge_line_width")
return frSolidInfill;
else if (opt_key == "top_surface_line_width")
return frTopSolidInfill;
else if (opt_key == "support_line_width")
@@ -67,6 +69,26 @@ double Flow::extrusion_width(const std::string& opt_key, const ConfigOptionFloat
{
assert(opt != nullptr);
auto opt_nozzle_diameters = config.option<ConfigOptionFloats>("nozzle_diameter");
if (opt_nozzle_diameters == nullptr)
throw_on_missing_variable(opt_key, "nozzle_diameter");
const float nozzle_diameter = float(opt_nozzle_diameters->get_at(first_printing_extruder));
if (opt_key == "bridge_line_width") {
if (opt->percent) {
const double bridge_width = opt->get_abs_value(nozzle_diameter);
if (bridge_width > 0.)
return bridge_width;
} else if (opt->value > 0.) {
return opt->value;
}
opt = config.option<ConfigOptionFloatOrPercent>("internal_solid_infill_line_width");
if (opt == nullptr)
throw_on_missing_variable(opt_key, "internal_solid_infill_line_width");
return extrusion_width("internal_solid_infill_line_width", opt, config, first_printing_extruder);
}
#if 0
// This is the logic used for skit / brim, but not for the rest of the 1st layer.
if (opt->value == 0. && first_layer) {
@@ -84,17 +106,13 @@ double Flow::extrusion_width(const std::string& opt_key, const ConfigOptionFloat
throw_on_missing_variable(opt_key, "line_width");
}
auto opt_nozzle_diameters = config.option<ConfigOptionFloats>("nozzle_diameter");
if (opt_nozzle_diameters == nullptr)
throw_on_missing_variable(opt_key, "nozzle_diameter");
if (opt->percent) {
return opt->get_abs_value(float(opt_nozzle_diameters->get_at(first_printing_extruder)));
return opt->get_abs_value(nozzle_diameter);
}
if (opt->value == 0.) {
// If user left option to 0, calculate a sane default width.
return auto_extrusion_width(opt_key_to_flow_role(opt_key), float(opt_nozzle_diameters->get_at(first_printing_extruder)));
return auto_extrusion_width(opt_key_to_flow_role(opt_key), nozzle_diameter);
}
return opt->value;
+143 -66
View File
@@ -2278,17 +2278,45 @@ namespace DoExport {
ooze_prevention.enable = print.config().ooze_prevention.value && ! print.config().single_extruder_multi_material;
}
// Count tool/filament changes across the print from the tool ordering. Used as a fallback when no
// wipe tower populated WipeTowerData::number_of_toolchanges (left at -1). Covers non-sequential
// prints without a wipe tower (manual swaps, toolchanger/IDEX). Note: sequential (by-object) prints
// leave print.tool_ordering() empty, so total_toolchanges stays 0 there (unchanged from before).
static int total_toolchanges_from_ordering(const ToolOrdering &tool_ordering)
{
int changes = 0;
int last = -1;
for (const LayerTools &lt : tool_ordering)
for (unsigned int extruder : lt.extruders) {
if (last >= 0 && int(extruder) != last)
++ changes;
last = int(extruder);
}
return changes;
}
// Total tool changes for the print, preferring the wipe-tower count and falling back to the tool
// ordering when no wipe tower populated it (number_of_toolchanges < 0).
static int resolve_total_toolchanges(const WipeTowerData &wipe_tower_data, const ToolOrdering &tool_ordering)
{
int changes = wipe_tower_data.number_of_toolchanges;
if (changes < 0)
changes = total_toolchanges_from_ordering(tool_ordering);
return std::max(0, changes);
}
// Fill in print_statistics and return formatted string containing filament statistics to be inserted into G-code comment section.
static std::string update_print_stats_and_format_filament_stats(
const bool has_wipe_tower,
const WipeTowerData &wipe_tower_data,
const std::vector<Extruder> &extruders,
PrintStatistics &print_statistics)
PrintStatistics &print_statistics,
const ToolOrdering &tool_ordering)
{
std::string filament_stats_string_out;
print_statistics.clear();
print_statistics.total_toolchanges = std::max(0, wipe_tower_data.number_of_toolchanges);
print_statistics.total_toolchanges = resolve_total_toolchanges(wipe_tower_data, tool_ordering);
if (! extruders.empty()) {
std::pair<std::string, unsigned int> out_filament_used_mm ("; filament used [mm] = ", 0);
std::pair<std::string, unsigned int> out_filament_used_cm3("; filament used [cm3] = ", 0);
@@ -2521,6 +2549,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
std::string top_gcode_template = print.config().file_start_gcode.value;
if (!top_gcode_template.empty()) {
DynamicConfig top_config;
// file_start_gcode runs before the parser copy that normally restores these, so set them here.
PlaceholderParser::update_timestamp(top_config);
PlaceholderParser::update_user_name(top_config);
top_config.set_key_value("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
top_config.set_key_value("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
std::string top_gcode = print.placeholder_parser().process(top_gcode_template, 0, &top_config);
@@ -2858,7 +2889,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// For the start / end G-code to do the priming and final filament pull in case there is no wipe tower provided.
this->placeholder_parser().set("has_wipe_tower", has_wipe_tower);
this->placeholder_parser().set("has_single_extruder_multi_material_priming", wipe_tower_type == WipeTowerType::Type2 && has_wipe_tower && print.config().single_extruder_multi_material_priming);
this->placeholder_parser().set("total_toolchanges", std::max(0, print.wipe_tower_data().number_of_toolchanges)); // Check for negative toolchanges (single extruder mode) and set to 0 (no tool change).
this->placeholder_parser().set("total_toolchanges", DoExport::resolve_total_toolchanges(print.wipe_tower_data(), print.tool_ordering()));
this->placeholder_parser().set("num_extruders", int(print.config().nozzle_diameter.values.size()));
this->placeholder_parser().set("retract_length", new ConfigOptionFloats(print.config().retraction_length));
@@ -3141,18 +3172,24 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
if (is_bbl_printers) {
this->_print_first_layer_extruder_temperatures(file, print, machine_start_gcode, initial_extruder_id, true);
}
// Orca: when activate_air_filtration is set on any extruder, find and set the highest during_print_exhaust_fan_speed
bool activate_air_filtration_during_print = false;
int during_print_exhaust_fan_speed = 0;
for (const auto &extruder : m_writer.extruders()) {
if (m_config.activate_air_filtration.get_at(extruder.id()) && m_config.activate_air_filtration_during_print.get_at(extruder.id())) {
activate_air_filtration_during_print = true;
during_print_exhaust_fan_speed = std::max(during_print_exhaust_fan_speed,
m_config.during_print_exhaust_fan_speed.get_at(extruder.id()));
// Orca: when air filtration is supported, check if it needs to be activated during printing and set the exhaust fan speed accordingly
if (m_config.support_air_filtration.value) {
bool activate_air_filtration_during_print = false;
int during_print_exhaust_fan_speed = 0;
// Orca: when activate_air_filtration is set on any extruder, find and set the highest during_print_exhaust_fan_speed
for (const auto &extruder : m_writer.extruders()) {
if (m_config.activate_air_filtration.get_at(extruder.id()) && m_config.activate_air_filtration_during_print.get_at(extruder.id())) {
activate_air_filtration_during_print = true;
during_print_exhaust_fan_speed = std::max(during_print_exhaust_fan_speed,
m_config.during_print_exhaust_fan_speed.get_at(extruder.id()));
}
}
if (activate_air_filtration_during_print)
file.write(m_writer.set_exhaust_fan(during_print_exhaust_fan_speed));
}
if (activate_air_filtration_during_print)
file.write(m_writer.set_exhaust_fan(during_print_exhaust_fan_speed, true));
print.throw_if_canceled();
@@ -3451,16 +3488,23 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
if (activate_chamber_temp_control && max_chamber_temp > 0)
file.write(m_writer.set_chamber_temperature(0, false)); //close chamber_temperature
bool activate_air_filtration_on_completion = false;
int complete_print_exhaust_fan_speed = 0;
for (const auto& extruder : m_writer.extruders()) {
if (m_config.activate_air_filtration.get_at(extruder.id()) && m_config.activate_air_filtration_on_completion.get_at(extruder.id())) {
activate_air_filtration_on_completion = true;
complete_print_exhaust_fan_speed = std::max(complete_print_exhaust_fan_speed, m_config.complete_print_exhaust_fan_speed.get_at(extruder.id()));
// Orca: when air filtration is supported, check if it needs to be activated after print completion and set the exhaust fan speed accordingly
if (m_config.support_air_filtration.value) {
bool activate_air_filtration_on_completion = false;
int complete_print_exhaust_fan_speed = 0;
// Orca: when activate_air_filtration is set on any extruder, find and set the highest complete_print_exhaust_fan_speed
for (const auto& extruder : m_writer.extruders()) {
if (m_config.activate_air_filtration.get_at(extruder.id()) && m_config.activate_air_filtration_on_completion.get_at(extruder.id())) {
activate_air_filtration_on_completion = true;
complete_print_exhaust_fan_speed = std::max(complete_print_exhaust_fan_speed, m_config.complete_print_exhaust_fan_speed.get_at(extruder.id()));
}
}
if (activate_air_filtration_on_completion)
file.write(m_writer.set_exhaust_fan(complete_print_exhaust_fan_speed));
}
if (activate_air_filtration_on_completion)
file.write(m_writer.set_exhaust_fan(complete_print_exhaust_fan_speed, true));
// adds tags for time estimators
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Last_Line_M73_Placeholder).c_str());
file.write_format("; EXECUTABLE_BLOCK_END\n\n");
@@ -3473,7 +3517,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
has_wipe_tower, print.wipe_tower_data(),
m_writer.extruders(),
// Modifies
print.m_print_statistics));
print.m_print_statistics,
// Const input (tool-change fallback for non-wipe-tower prints)
print.tool_ordering()));
print.m_print_statistics.initial_tool = initial_extruder_id;
if (!is_bbl_printers) {
file.write_format("; total filament used [g] = %.2lf\n",
@@ -4729,6 +4775,7 @@ LayerResult GCode::process_layer(
// Group extrusions by an extruder, then by an object, an island and a region.
std::map<unsigned int, std::vector<ObjectByExtruder>> by_extruder;
std::vector<std::unique_ptr<ExtrusionEntityCollection>> split_perimeter_storage;
bool is_anything_overridden = const_cast<LayerTools&>(layer_tools).wiping_extrusions().is_anything_overridden();
for (const LayerToPrint &layer_to_print : layers) {
if (layer_to_print.support_layer != nullptr) {
@@ -4884,55 +4931,83 @@ LayerResult GCode::process_layer(
if (extrusions->entities.empty()) // This shouldn't happen but first_point() would fail.
continue;
// This extrusion is part of certain Region, which tells us which extruder should be used for it:
int correct_extruder_id = layer_tools.extruder(*extrusions, region);
auto process_extrusions = [&](const ExtrusionEntityCollection *current_extrusions,
const ExtrusionEntityCollection *overrides_key,
bool use_overrides) {
// This extrusion is part of certain Region, which tells us which extruder should be used for it.
int correct_extruder_id = layer_tools.extruder(*current_extrusions, region);
// Let's recover vector of extruder overrides:
const WipingExtrusions::ExtruderPerCopy *entity_overrides = nullptr;
if (! layer_tools.has_extruder(correct_extruder_id)) {
// this entity is not overridden, but its extruder is not in layer_tools - we'll print it
// by last extruder on this layer (could happen e.g. when a wiping object is taller than others - dontcare extruders are eradicated from layer_tools)
correct_extruder_id = layer_tools.extruders.back();
}
printing_extruders.clear();
if (is_anything_overridden) {
entity_overrides = const_cast<LayerTools&>(layer_tools).wiping_extrusions().get_extruder_overrides(extrusions, layer_to_print.original_object, correct_extruder_id, layer_to_print.object()->instances().size());
if (entity_overrides == nullptr) {
const WipingExtrusions::ExtruderPerCopy *entity_overrides = nullptr;
if (! layer_tools.has_extruder(correct_extruder_id)) {
// this entity is not overridden, but its extruder is not in layer_tools - we'll print it
// by last extruder on this layer (could happen e.g. when a wiping object is taller than others - dontcare extruders are eradicated from layer_tools)
correct_extruder_id = layer_tools.extruders.back();
}
printing_extruders.clear();
if (is_anything_overridden && use_overrides) {
entity_overrides = const_cast<LayerTools&>(layer_tools).wiping_extrusions().get_extruder_overrides(overrides_key, layer_to_print.original_object, correct_extruder_id, layer_to_print.object()->instances().size());
if (entity_overrides == nullptr) {
printing_extruders.emplace_back(correct_extruder_id);
} else {
printing_extruders.reserve(entity_overrides->size());
for (int extruder : *entity_overrides)
printing_extruders.emplace_back(extruder >= 0 ?
// at least one copy is overridden to use this extruder
extruder :
// at least one copy would normally be printed with this extruder (see get_extruder_overrides function for explanation)
static_cast<unsigned int>(- extruder - 1));
Slic3r::sort_remove_duplicates(printing_extruders);
}
} else {
printing_extruders.emplace_back(correct_extruder_id);
} else {
printing_extruders.reserve(entity_overrides->size());
for (int extruder : *entity_overrides)
printing_extruders.emplace_back(extruder >= 0 ?
// at least one copy is overridden to use this extruder
extruder :
// at least one copy would normally be printed with this extruder (see get_extruder_overrides function for explanation)
static_cast<unsigned int>(- extruder - 1));
Slic3r::sort_remove_duplicates(printing_extruders);
}
} else
printing_extruders.emplace_back(correct_extruder_id);
// Now we must add this extrusion into the by_extruder map, once for each extruder that will print it:
for (unsigned int extruder : printing_extruders)
{
std::vector<ObjectByExtruder::Island> &islands = object_islands_by_extruder(
by_extruder,
extruder,
&layer_to_print - layers.data(),
layers.size(), n_slices+1);
for (size_t i = 0; i <= n_slices; ++ i) {
bool last = i == n_slices;
size_t island_idx = last ? n_slices : slices_test_order[i];
if (// extrusions->first_point does not fit inside any slice
last ||
// extrusions->first_point fits inside ith slice
point_inside_surface(island_idx, extrusions->first_point())) {
if (islands[island_idx].by_region.empty())
islands[island_idx].by_region.assign(print.num_print_regions(), ObjectByExtruder::Island::Region());
islands[island_idx].by_region[region.print_region_id()].append(entity_type, extrusions, entity_overrides);
break;
// Now we must add this extrusion into the by_extruder map, once for each extruder that will print it.
for (unsigned int extruder : printing_extruders) {
std::vector<ObjectByExtruder::Island> &islands = object_islands_by_extruder(
by_extruder,
extruder,
&layer_to_print - layers.data(),
layers.size(), n_slices + 1);
for (size_t i = 0; i <= n_slices; ++i) {
bool last = i == n_slices;
size_t island_idx = last ? n_slices : slices_test_order[i];
if (last || point_inside_surface(island_idx, current_extrusions->first_point())) {
if (islands[island_idx].by_region.empty())
islands[island_idx].by_region.assign(print.num_print_regions(), ObjectByExtruder::Island::Region());
islands[island_idx].by_region[region.print_region_id()].append(entity_type, current_extrusions, entity_overrides);
break;
}
}
}
};
bool split_mixed_perimeters =
entity_type == ObjectByExtruder::Island::Region::PERIMETERS &&
region.config().outer_wall_filament_id.value != region.config().inner_wall_filament_id.value &&
extrusions->role() == erMixed;
if (split_mixed_perimeters) {
auto outer_perimeters = std::make_unique<ExtrusionEntityCollection>();
auto inner_perimeters = std::make_unique<ExtrusionEntityCollection>();
for (const ExtrusionEntity *entity : extrusions->entities) {
const ExtrusionRole role = entity->role();
if (role == erExternalPerimeter || role == erOverhangPerimeter)
outer_perimeters->append(*entity);
else if (role == erPerimeter)
inner_perimeters->append(*entity);
}
if (!outer_perimeters->entities.empty()) {
split_perimeter_storage.emplace_back(std::move(outer_perimeters));
process_extrusions(split_perimeter_storage.back().get(), nullptr, false);
}
if (!inner_perimeters->entities.empty()) {
split_perimeter_storage.emplace_back(std::move(inner_perimeters));
process_extrusions(split_perimeter_storage.back().get(), nullptr, false);
}
} else {
process_extrusions(extrusions, extrusions, true);
}
}
}
@@ -6128,7 +6203,9 @@ std::string GCode::extrude_support(const ExtrusionEntityCollection &support_fill
if (extrusions.empty())
return gcode;
chain_and_reorder_extrusion_entities(extrusions, m_last_pos.to_point());
//ORCA: Respect no_sort to preserve support base outline->fill order.
if (!support_fills.no_sort)
chain_and_reorder_extrusion_entities(extrusions, m_last_pos.to_point());
const double support_speed = m_config.support_speed.value;
const double support_interface_speed = m_config.get_abs_value("support_interface_speed");
+59 -33
View File
@@ -80,40 +80,54 @@ bool check_filament_printable_after_group(const std::vector<unsigned int> &used_
}
// Return a zero based extruder from the region, or extruder_override if overriden.
unsigned int LayerTools::wall_filament(const PrintRegion &region) const
unsigned int LayerTools::wall_extruder_id(const PrintRegion &region) const
{
assert(region.config().wall_filament.value > 0);
return ((this->extruder_override == 0) ? region.config().wall_filament.value : this->extruder_override) - 1;
assert(region.config().outer_wall_filament_id.value > 0);
return ((this->extruder_override == 0) ? region.config().outer_wall_filament_id.value : this->extruder_override) - 1;
}
unsigned int LayerTools::sparse_infill_filament(const PrintRegion &region) const
unsigned int LayerTools::sparse_infill_filament_id(const PrintRegion &region) const
{
assert(region.config().sparse_infill_filament.value > 0);
return ((this->extruder_override == 0) ? region.config().sparse_infill_filament.value : this->extruder_override) - 1;
assert(region.config().sparse_infill_filament_id.value > 0);
return ((this->extruder_override == 0) ? region.config().sparse_infill_filament_id.value : this->extruder_override) - 1;
}
unsigned int LayerTools::solid_infill_filament(const PrintRegion &region) const
unsigned int LayerTools::internal_solid_filament_id(const PrintRegion &region) const
{
assert(region.config().solid_infill_filament.value > 0);
return ((this->extruder_override == 0) ? region.config().solid_infill_filament.value : this->extruder_override) - 1;
assert(region.config().internal_solid_filament_id.value > 0);
return ((this->extruder_override == 0) ? region.config().internal_solid_filament_id.value : this->extruder_override) - 1;
}
// Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden.
unsigned int LayerTools::extruder(const ExtrusionEntityCollection &extrusions, const PrintRegion &region) const
{
assert(region.config().wall_filament.value > 0);
assert(region.config().sparse_infill_filament.value > 0);
assert(region.config().solid_infill_filament.value > 0);
assert(region.config().outer_wall_filament_id.value > 0);
assert(region.config().sparse_infill_filament_id.value > 0);
assert(region.config().internal_solid_filament_id.value > 0);
assert(region.config().top_surface_filament_id.value > 0);
assert(region.config().bottom_surface_filament_id.value > 0);
// 1 based extruder ID.
unsigned int extruder = 1;
if (this->extruder_override == 0) {
if (extrusions.has_infill()) {
if (extrusions.has_solid_infill())
extruder = region.config().solid_infill_filament;
if (extrusions.has_solid_infill()) {
ExtrusionRole role = extrusions.role();
if (role == erTopSolidInfill || role == erIroning)
extruder = region.config().top_surface_filament_id;
else if (role == erBottomSurface)
extruder = region.config().bottom_surface_filament_id;
else
extruder = region.config().internal_solid_filament_id;
} else {
extruder = region.config().sparse_infill_filament_id;
}
} else {
const ExtrusionRole role = extrusions.role();
if (role == erPerimeter)
extruder = region.config().inner_wall_filament_id.value;
else
extruder = region.config().sparse_infill_filament;
} else
extruder = region.config().wall_filament.value;
extruder = region.config().outer_wall_filament_id.value;
}
} else
extruder = this->extruder_override;
@@ -527,7 +541,7 @@ std::vector<unsigned int> ToolOrdering::generate_first_layer_tool_order(const Pr
return tool_order;
for (auto layerm : target_layer->regions()) {
int extruder_id = layerm->region().config().option("wall_filament")->getInt();
int extruder_id = layerm->region().config().option("outer_wall_filament_id")->getInt();
for (auto expoly : layerm->raw_slices) {
const double nozzle_diameter = print.config().nozzle_diameter.get_at(0);
@@ -591,7 +605,7 @@ std::vector<unsigned int> ToolOrdering::generate_first_layer_tool_order(const Pr
return tool_order;
for (auto layerm : target_layer->regions()) {
int extruder_id = layerm->region().config().option("wall_filament")->getInt();
int extruder_id = layerm->region().config().option("outer_wall_filament_id")->getInt();
for (auto expoly : layerm->raw_slices) {
const double nozzle_diameter = object.print()->config().nozzle_diameter.get_at(0);
const coordf_t line_width = object.config().get_abs_value("line_width", nozzle_diameter);
@@ -682,24 +696,32 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
}
if (something_nonoverriddable){
layer_tools.extruders.emplace_back((extruder_override == 0) ? region.config().wall_filament.value : extruder_override);
layer_tools.extruders.emplace_back((extruder_override == 0) ? region.config().outer_wall_filament_id.value : extruder_override);
if (extruder_override == 0 && region.config().wall_loops.value > 1)
layer_tools.extruders.emplace_back(region.config().inner_wall_filament_id.value);
if (layerCount == 0) {
firstLayerExtruders.emplace_back((extruder_override == 0) ? region.config().wall_filament.value : extruder_override);
firstLayerExtruders.emplace_back((extruder_override == 0) ? region.config().outer_wall_filament_id.value : extruder_override);
}
}
layer_tools.has_object = true;
}
bool has_infill = false;
bool has_solid_infill = false;
bool has_infill = false;
bool has_internal_solid = false;
bool has_top_solid_surface = false;
bool has_bottom_surface = false;
bool something_nonoverriddable = false;
for (const ExtrusionEntity *ee : layerm->fills.entities) {
// fill represents infill extrusions of a single island.
const auto *fill = dynamic_cast<const ExtrusionEntityCollection*>(ee);
ExtrusionRole role = fill->entities.empty() ? erNone : fill->entities.front()->role();
if (is_solid_infill(role))
has_solid_infill = true;
if (role == erTopSolidInfill || role == erIroning)
has_top_solid_surface = true;
else if (role == erBottomSurface)
has_bottom_surface = true;
else if (is_solid_infill(role))
has_internal_solid = true;
else if (role != erNone)
has_infill = true;
@@ -711,14 +733,18 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
if (something_nonoverriddable || !m_print_config_ptr) {
if (extruder_override == 0) {
if (has_solid_infill)
layer_tools.extruders.emplace_back(region.config().solid_infill_filament);
if (has_internal_solid)
layer_tools.extruders.emplace_back(region.config().internal_solid_filament_id);
if (has_top_solid_surface)
layer_tools.extruders.emplace_back(region.config().top_surface_filament_id);
if (has_bottom_surface)
layer_tools.extruders.emplace_back(region.config().bottom_surface_filament_id);
if (has_infill)
layer_tools.extruders.emplace_back(region.config().sparse_infill_filament);
} else if (has_solid_infill || has_infill)
layer_tools.extruders.emplace_back(region.config().sparse_infill_filament_id);
} else if (has_internal_solid || has_top_solid_surface || has_bottom_surface || has_infill)
layer_tools.extruders.emplace_back(extruder_override);
}
if (has_solid_infill || has_infill)
if (has_internal_solid || has_top_solid_surface || has_bottom_surface || has_infill)
layer_tools.has_object = true;
}
layerCount++;
@@ -1657,7 +1683,7 @@ float WipingExtrusions::mark_wiping_extrusions(const Print& print, unsigned int
if (wipe_into_infill_only && ! is_infill_first)
// In this case we must check that the original extruder is used on this layer before the one we are overridding
// (and the perimeters will be finished before the infill is printed):
if (!lt.is_extruder_order(lt.wall_filament(region), new_extruder))
if (!lt.is_extruder_order(lt.wall_extruder_id(region), new_extruder))
continue;
if ((!is_entity_overridden(fill, object, copy) && fill->total_volume() > min_infill_volume))
@@ -1775,8 +1801,8 @@ void WipingExtrusions::ensure_perimeters_infills_order(const Print& print)
if (is_infill_first
//BBS
//|| object->config().flush_into_objects // in this case the perimeter is overridden, so we can override by the last one safely
|| lt.is_extruder_order(lt.wall_filament(region), last_nonsoluble_extruder // !infill_first, but perimeter is already printed when last extruder prints
|| ! lt.has_extruder(lt.sparse_infill_filament(region)))) // we have to force override - this could violate infill_first (FIXME)
|| lt.is_extruder_order(lt.wall_extruder_id(region), last_nonsoluble_extruder // !infill_first, but perimeter is already printed when last extruder prints
|| ! lt.has_extruder(lt.sparse_infill_filament_id(region)))) // we have to force override - this could violate infill_first (FIXME)
set_extruder_override(fill, object, copy, (is_infill_first ? first_nonsoluble_extruder : last_nonsoluble_extruder), num_of_copies);
else {
// In this case we can (and should) leave it to be printed normally.
+3 -3
View File
@@ -139,9 +139,9 @@ public:
bool has_extruder(unsigned int extruder) const { return std::find(this->extruders.begin(), this->extruders.end(), extruder) != this->extruders.end(); }
// Return a zero based extruder from the region, or extruder_override if overriden.
unsigned int wall_filament(const PrintRegion &region) const;
unsigned int sparse_infill_filament(const PrintRegion &region) const;
unsigned int solid_infill_filament(const PrintRegion &region) const;
unsigned int wall_extruder_id(const PrintRegion &region) const;
unsigned int sparse_infill_filament_id(const PrintRegion &region) const;
unsigned int internal_solid_filament_id(const PrintRegion &region) const;
// Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden.
unsigned int extruder(const ExtrusionEntityCollection &extrusions, const PrintRegion &region) const;
+8 -8
View File
@@ -3881,7 +3881,7 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
for (auto &used : m_used_filament_length) // reset used filament stats
used = 0.f;
int wall_filament = get_wall_filament_for_all_layer();
int wall_filament_id = get_wall_filament_for_all_layer();
std::vector<WipeTower::ToolChangeResult> layer_result;
int index = 0;
@@ -3909,24 +3909,24 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
ToolChangeResult finish_layer_tcr;
ToolChangeResult timelapse_wall;
auto get_wall_filament_for_this_layer = [this, &layer, &wall_filament]() -> int {
auto get_wall_filament_for_this_layer = [this, &layer, &wall_filament_id]() -> int {
if (layer.tool_changes.size() == 0)
return -1;
int candidate_id = -1;
for (size_t idx = 0; idx < layer.tool_changes.size(); ++idx) {
if (idx == 0) {
if (layer.tool_changes[idx].old_tool == wall_filament)
return wall_filament;
else if (m_filpar[layer.tool_changes[idx].old_tool].category == m_filpar[wall_filament].category) {
if (layer.tool_changes[idx].old_tool == wall_filament_id)
return wall_filament_id;
else if (m_filpar[layer.tool_changes[idx].old_tool].category == m_filpar[wall_filament_id].category) {
candidate_id = layer.tool_changes[idx].old_tool;
}
}
if (layer.tool_changes[idx].new_tool == wall_filament) {
return wall_filament;
if (layer.tool_changes[idx].new_tool == wall_filament_id) {
return wall_filament_id;
}
if ((candidate_id == -1) && (m_filpar[layer.tool_changes[idx].new_tool].category == m_filpar[wall_filament].category))
if ((candidate_id == -1) && (m_filpar[layer.tool_changes[idx].new_tool].category == m_filpar[wall_filament_id].category))
candidate_id = layer.tool_changes[idx].new_tool;
}
return candidate_id == -1 ? layer.tool_changes[0].new_tool : candidate_id;
+9 -3
View File
@@ -1154,13 +1154,19 @@ std::string GCodeWriter::set_additional_fan(unsigned int speed)
return gcode.str();
}
std::string GCodeWriter::set_exhaust_fan( int speed,bool add_eol)
std::string GCodeWriter::set_exhaust_fan(int speed)
{
std::ostringstream gcode;
gcode << "M106" << " P3" << " S" << (int)(speed / 100.0 * 255);
if(add_eol)
gcode << "\n";
if (GCodeWriter::full_gcode_comment) {
if (speed == 0)
gcode << " ; disable exhaust fan ";
else
gcode << " ; enable exhaust fan ";
}
gcode << "\n";
return gcode.str();
}
+1 -1
View File
@@ -106,7 +106,7 @@ public:
std::string set_fan(unsigned int speed) const;
//BBS: set additional fan speed for BBS machine only
static std::string set_additional_fan(unsigned int speed);
static std::string set_exhaust_fan(int speed,bool add_eol);
static std::string set_exhaust_fan(int speed);
//BBS
void set_object_start_str(std::string start_string) { m_gcode_label_objects_start = start_string; }
bool is_object_start_str_empty() { return m_gcode_label_objects_start.empty(); }
+2 -1
View File
@@ -141,7 +141,8 @@ bool Layer::is_perimeter_compatible(const PrintRegion& a, const PrintRegion& b)
const PrintRegionConfig& config = a.config();
const PrintRegionConfig& other_config = b.config();
return config.wall_filament == other_config.wall_filament
return config.outer_wall_filament_id == other_config.outer_wall_filament_id
&& config.inner_wall_filament_id == other_config.inner_wall_filament_id
&& config.wall_loops == other_config.wall_loops
&& config.wall_sequence == other_config.wall_sequence
&& config.is_infill_first == other_config.is_infill_first
+57 -10
View File
@@ -34,16 +34,26 @@ Flow LayerRegion::bridging_flow(FlowRole role, bool thick_bridge) const
const PrintRegionConfig &region_config = region.config();
const PrintObject &print_object = *this->layer()->object();
Flow bridge_flow;
// Here this->extruder(role) - 1 may underflow to MAX_INT, but then the get_at() will fall back to zero'th element, so everything is all right.
auto nozzle_diameter = float(print_object.print()->config().nozzle_diameter.get_at(region.extruder(role) - 1));
const ConfigOptionFloatOrPercent& bridge_width_opt = region_config.bridge_line_width;
const double bridge_width = bridge_width_opt.get_abs_value(nozzle_diameter);
const bool has_bridge_width = bridge_width > 0.;
const double bridge_flow_ratio = region_config.bridge_flow;
if (thick_bridge) {
// The old Slic3r way (different from all other slicers): Use rounded extrusions.
// Get the configured nozzle_diameter for the extruder associated to the flow role requested.
// Here this->extruder(role) - 1 may underflow to MAX_INT, but then the get_at() will follback to zero'th element, so everything is all right.
// Applies default bridge spacing.
bridge_flow = Flow::bridging_flow(float(sqrt(region_config.bridge_flow)) * nozzle_diameter, nozzle_diameter);
float thread_diameter = has_bridge_width ? float(bridge_width) : nozzle_diameter;
if (bridge_flow_ratio > 0.)
thread_diameter *= float(sqrt(bridge_flow_ratio));
bridge_flow = Flow::bridging_flow(thread_diameter, nozzle_diameter);
} else {
// The same way as other slicers: Use normal extrusions. Apply bridge_flow while maintaining the original spacing.
bridge_flow = this->flow(role).with_flow_ratio(region_config.bridge_flow);
Flow base_flow = this->flow(role);
if (has_bridge_width)
base_flow = Flow(float(bridge_width), base_flow.height(), nozzle_diameter);
bridge_flow = base_flow.with_flow_ratio(bridge_flow_ratio);
}
return bridge_flow;
@@ -83,6 +93,12 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe
(this->layer()->id() >= size_t(region_config.bottom_shell_layers.value) &&
this->layer()->print_z >= region_config.bottom_shell_thickness - EPSILON);
double model_rotation_rad = 0.0;
if (region_config.align_infill_direction_to_model) {
auto m = this->layer()->object()->trafo().matrix();
model_rotation_rad = std::atan2((double)m(1, 0), (double)m(0, 0));
}
PerimeterGenerator g(
// input:
&slices,
@@ -94,6 +110,7 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe
&this->layer()->object()->config(),
&print_config,
spiral_mode,
model_rotation_rad,
// output:
&this->perimeters,
@@ -517,10 +534,27 @@ void LayerRegion::process_external_surfaces(const Layer *lower_layer, const Poly
SurfaceCollection bridges;
{
BOOST_LOG_TRIVIAL(trace) << "Processing external surface, detecting bridges. layer" << this->layer()->print_z;
const double custom_angle = this->region().config().bridge_angle.value;
bridges.surfaces = custom_angle > 0 ?
expand_merge_surfaces(this->fill_surfaces.surfaces, stBottomBridge, expansion_zones, closing_radius, Geometry::deg2rad(custom_angle)) :
// ORCA: Relative/Align Bridge Angle
const auto &region_config = this->region().config();
const double custom_angle_deg = region_config.bridge_angle.value;
const bool relative_angle = region_config.relative_bridge_angle.value;
const double custom_angle_rad = Geometry::deg2rad(custom_angle_deg);
double align_offset_rad = 0.0;
if (region_config.align_infill_direction_to_model) {
auto m = this->layer()->object()->trafo().matrix();
align_offset_rad = std::atan2((double)m(1, 0), (double)m(0, 0));
}
bridges.surfaces = (custom_angle_deg > 0.0 && !relative_angle) ?
expand_merge_surfaces(this->fill_surfaces.surfaces, stBottomBridge, expansion_zones, closing_radius, custom_angle_rad + align_offset_rad) :
expand_bridges_detect_orientations(this->fill_surfaces.surfaces, expansion_zones, closing_radius);
if (custom_angle_deg > 0.0 && relative_angle) {
for (Surface &bridge_surface : bridges.surfaces) {
if (bridge_surface.bridge_angle >= 0)
bridge_surface.bridge_angle += custom_angle_rad;
}
}
BOOST_LOG_TRIVIAL(trace) << "Processing external surface, detecting bridges - done";
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{
@@ -782,12 +816,25 @@ void LayerRegion::process_external_surfaces(const Layer *lower_layer, const Poly
// would get merged into a single one while they need different directions
// also, supply the original expolygon instead of the grown one, because in case
// of very thin (but still working) anchors, the grown expolygon would go beyond them
double custom_angle = Geometry::deg2rad(this->region().config().bridge_angle.value);
if (custom_angle > 0.0) {
bridges[idx_last].bridge_angle = custom_angle;
// ORCA: Relative/Align Bridge Angle
const auto &region_config = this->region().config();
const double custom_angle_deg = region_config.bridge_angle.value;
const bool relative_angle = region_config.relative_bridge_angle.value;
const double custom_angle_rad = Geometry::deg2rad(custom_angle_deg);
double align_offset_rad = 0.0;
if (region_config.align_infill_direction_to_model) {
auto m = this->layer()->object()->trafo().matrix();
align_offset_rad = std::atan2((double)m(1, 0), (double)m(0, 0));
}
if (custom_angle_deg > 0.0 && !relative_angle) {
bridges[idx_last].bridge_angle = custom_angle_rad + align_offset_rad;
} else {
auto [bridging_dir, unsupported_dist] = detect_bridging_direction(to_polygons(initial), to_polygons(lower_layer->lslices));
bridges[idx_last].bridge_angle = PI + std::atan2(bridging_dir.y(), bridging_dir.x());
if (custom_angle_deg > 0.0 && relative_angle)
bridges[idx_last].bridge_angle += custom_angle_rad;
}
/*
+1 -1
View File
@@ -1345,7 +1345,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
if (const PrintRegionConfig &config = region->region().config();
// color_idx == 0 means "don't know" extruder aka the underlying extruder.
// As this region may split existing regions, we collect statistics over all regions for color_idx == 0.
color_idx == 0 || config.wall_filament == int(color_idx)) {
color_idx == 0 || config.outer_wall_filament_id == int(color_idx)) {
//BBS: the extrusion line width is outer wall rather than inner wall
const double nozzle_diameter = print_object.print()->config().nozzle_diameter.get_at(0);
double outer_wall_line_width = config.get_abs_value("outer_wall_line_width", nozzle_diameter);
+105 -137
View File
@@ -620,7 +620,7 @@ void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExP
// get the real top surface
ExPolygons grown_lower_slices;
ExPolygons bridge_checker;
auto nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->wall_filament - 1);
auto nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->outer_wall_filament_id - 1);
// Check whether surface be bridge or not
if (this->lower_slices != NULL) {
// BBS: get the Polygons below the polygon this layer
@@ -1173,7 +1173,7 @@ void PerimeterGenerator::process_classic()
// We consider overhang any part where the entire nozzle diameter is not supported by the
// lower layer, so we take lower slices and offset them by half the nozzle diameter used
// in the current layer
double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->wall_filament - 1);
double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->outer_wall_filament_id - 1);
m_lower_slices_polygons = offset(*this->lower_slices, float(scale_(+nozzle_diameter / 2)));
}
@@ -1727,9 +1727,12 @@ void PerimeterGenerator::add_infill_contour_for_arachne( ExPolygons infil
// Orca: sacrificial bridge layer algorithm ported from SuperSlicer
void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perimeter_spacing, coord_t ext_perimeter_width)
{
if (this->config->counterbore_hole_bridging == chbNone)
return; // return if counterbore hole is not enabled
//store surface for bridge infill to avoid unsupported perimeters (but the first one, this one is always good)
if (this->config->counterbore_hole_bridging != chbNone
&& this->lower_slices != NULL && !this->lower_slices->empty()) {
if (this->lower_slices != NULL && !this->lower_slices->empty()) {
const coordf_t bridged_infill_margin = scale_(BRIDGE_INFILL_MARGIN);
for (size_t surface_idx = 0; surface_idx < all_surfaces.size(); surface_idx++) {
@@ -1738,11 +1741,8 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
//compute our unsupported surface
ExPolygons unsupported = diff_ex(last, *this->lower_slices, ApplySafetyOffset::Yes);
if (!unsupported.empty()) {
// remove small overhangs (when using chbFilled we need to be less aggressive in removing small overhangs,
// to avoid affecting bridging detection.)
const int outset_divisor = this->config->counterbore_hole_bridging.value == chbFilled ? 2 : 1;
ExPolygons unsupported_filtered = offset2_ex(unsupported, double(-perimeter_spacing),
double(perimeter_spacing) / outset_divisor);
//remove small overhangs
ExPolygons unsupported_filtered = offset2_ex(unsupported, double(-perimeter_spacing), double(perimeter_spacing));
if (!unsupported_filtered.empty()) {
//to_draw.insert(to_draw.end(), last.begin(), last.end());
@@ -1759,13 +1759,24 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
for (ExPolygon unsupported : unsupported_filtered) {
BridgeDetector detector{ unsupported,
lower_island.expolygons,
perimeter_spacing };
if (detector.detect_angle(Geometry::deg2rad(this->config->bridge_angle.value)))
perimeter_spacing / 4}; // Use a finer BridgeDetector. This affects coverage resolution, not extrusion spacing.
// ORCA: Relative/Align Bridge Angle
const double custom_angle_deg = this->config->bridge_angle.value;
const bool relative_angle = this->config->relative_bridge_angle.value;
const double detect_angle_rad = (custom_angle_deg > 0.0 && !relative_angle)
? Geometry::deg2rad(custom_angle_deg) +
(this->config->align_infill_direction_to_model ? this->m_model_rotation_rad : 0.0)
: 0.0;
if (detector.detect_angle(detect_angle_rad))
expolygons_append(bridgeable, union_ex(detector.coverage(-1, true)));
}
if (!bridgeable.empty()) {
//check if we get everything or just the bridgeable area
if (/*this->config->counterbore_hole_bridging.value == chbNoPeri || */this->config->counterbore_hole_bridging.value == chbFilled) {
if (!bridgeable.empty() && !surface->expolygon.holes.empty()) { // keep out if cannot be bridged or no holes to bridge
const coordf_t bridge_anchor_offset = std::min({bridged_infill_margin, coordf_t(perimeter_spacing), coordf_t(ext_perimeter_width)});
// Handle filled vs partial counterbore bridging modes.
if (this->config->counterbore_hole_bridging.value == chbFilled) {
unsupported_filtered = offset_ex(unsupported_filtered, -perimeter_spacing); // shrink it to survive the strict bridge-candidate filter
//we bridge everything, even the not-bridgeable bits
for (size_t i = 0; i < unsupported_filtered.size();) {
ExPolygon& poly_unsupp = *(unsupported_filtered.begin() + i);
@@ -1785,139 +1796,96 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
unsupported_filtered.erase(unsupported_filtered.begin() + i);
}
}
unsupported_filtered = intersection_ex(last,
offset_ex(unsupported_filtered, 0.5 * double(bridged_infill_margin)));
if (this->config->counterbore_hole_bridging.value == chbFilled) {
for (ExPolygon& expol : unsupported_filtered) {
//check if the holes won't be covered by the upper layer
//TODO: if we want to do that, we must modify the geometry before making perimeters.
//if (this->upper_slices != nullptr && !this->upper_slices->expolygons.empty()) {
// for (Polygon &poly : expol.holes) poly.make_counter_clockwise();
// float perimeterwidth = this->config->perimeters == 0 ? 0 : (this->ext_perimeter_flow.scaled_width() + (this->config->perimeters - 1) + this->perimeter_flow.scaled_spacing());
// std::cout << "test upper slices with perimeterwidth=" << perimeterwidth << "=>" << offset_ex(this->upper_slices->expolygons, -perimeterwidth).size();
// if (intersection(Polygons() = { expol.holes }, to_polygons(offset_ex(this->upper_slices->expolygons, -this->ext_perimeter_flow.scaled_width() / 2))).empty()) {
// std::cout << " EMPTY";
// expol.holes.clear();
// } else {
// }
// std::cout << "\n";
//} else {
expol.holes.clear();
//}
//detect inside volume
for (size_t surface_idx_other = 0; surface_idx_other < all_surfaces.size(); surface_idx_other++) {
if (surface_idx == surface_idx_other) continue;
if (intersection_ex(ExPolygons() = { expol }, ExPolygons() = { all_surfaces[surface_idx_other].expolygon }).size() > 0) {
//this means that other_surf was inside an expol holes
//as we removed them, we need to add a new one
ExPolygons new_poly = offset2_ex(ExPolygons{ all_surfaces[surface_idx_other].expolygon }, double(-bridged_infill_margin - perimeter_spacing), double(perimeter_spacing));
if (new_poly.size() == 1) {
all_surfaces[surface_idx_other].expolygon = new_poly[0];
expol.holes.push_back(new_poly[0].contour);
unsupported_filtered = offset_ex(unsupported_filtered, perimeter_spacing + bridge_anchor_offset); // restore it back to its original size and add anchor
unsupported_filtered = intersection_ex(last, unsupported_filtered); // clamp to the original surface, to avoid creating new unsupported areas
for (ExPolygon& expol : unsupported_filtered) {
// Remove holes that need sacrificial fill, but keep holes
// whose wall is already supported by the lower layer.
const float hole_wall_width = float(ext_perimeter_width / 2);
for (size_t hole_idx = 0; hole_idx < expol.holes.size();) {
Polygon hole_area_contour = expol.holes[hole_idx];
hole_area_contour.make_counter_clockwise();
const ExPolygons hole_area = { ExPolygon(hole_area_contour) };
ExPolygons hole_wall_area = diff_ex(
offset_ex(hole_area_contour, hole_wall_width),
hole_area,
ApplySafetyOffset::Yes);
hole_wall_area = intersection_ex(hole_wall_area, ExPolygons{ expol }, ApplySafetyOffset::Yes);
if (!hole_wall_area.empty() &&
intersection_ex(hole_wall_area, *this->lower_slices, ApplySafetyOffset::Yes).empty())
expol.holes.erase(expol.holes.begin() + hole_idx);
// After erase(), the next hole shifts into the same index. So hole_idx
// must not be incremented, otherwise the next hole would be skipped.
else
++hole_idx; // keep this hole, it won't be bridged, so we need to keep it as a hole
}
//detect inside volume
for (size_t surface_idx_other = 0; surface_idx_other < all_surfaces.size(); surface_idx_other++) {
if (surface_idx == surface_idx_other) continue;
if (intersection_ex(ExPolygons() = { expol }, ExPolygons() = { all_surfaces[surface_idx_other].expolygon }).size() > 0) {
//this means that other_surf was inside an expol holes
//as we removed them, we need to add a new one
ExPolygons new_poly = offset2_ex(ExPolygons{ all_surfaces[surface_idx_other].expolygon }, double(-bridged_infill_margin - perimeter_spacing), double(perimeter_spacing));
if (new_poly.size() == 1) {
all_surfaces[surface_idx_other].expolygon = new_poly[0];
expol.holes.push_back(new_poly[0].contour);
expol.holes.back().make_clockwise();
} else {
for (size_t idx = 0; idx < new_poly.size(); idx++) {
Surface new_surf = all_surfaces[surface_idx_other];
new_surf.expolygon = new_poly[idx];
all_surfaces.push_back(new_surf);
expol.holes.push_back(new_poly[idx].contour);
expol.holes.back().make_clockwise();
} else {
for (size_t idx = 0; idx < new_poly.size(); idx++) {
Surface new_surf = all_surfaces[surface_idx_other];
new_surf.expolygon = new_poly[idx];
all_surfaces.push_back(new_surf);
expol.holes.push_back(new_poly[idx].contour);
expol.holes.back().make_clockwise();
}
all_surfaces.erase(all_surfaces.begin() + surface_idx_other);
if (surface_idx_other < surface_idx) {
surface_idx--;
surface = &all_surfaces[surface_idx];
}
surface_idx_other--;
}
all_surfaces.erase(all_surfaces.begin() + surface_idx_other);
if (surface_idx_other < surface_idx) {
surface_idx--;
surface = &all_surfaces[surface_idx];
}
surface_idx_other--;
}
}
}
}
//TODO: add other polys as holes inside this one (-margin)
} else if (/*this->config->counterbore_hole_bridging.value == chbBridgesOverhangs || */this->config->counterbore_hole_bridging.value == chbBridges) {
// Partially bridged counterbore handling should not rewrite generic bridge islands
// because by doing so regular bridges will lose their overhang-wall perimeters.
if (surface->expolygon.holes.empty()) {
unsupported_filtered.clear(); // "Partially bridged" only applies to hole-bearing bridge islands.
continue;
}
//simplify to avoid most of artefacts from printing lines.
ExPolygons bridgeable_simplified;
} else { // if(this->config->counterbore_hole_bridging.value == chbBridges)
// Orca: Partial counterbore bridging is mask-based. Preserve the supported
// remainder (`last`) and use simplified BridgeDetector coverage to derive the
// bridgeable counterbore span. The span is grown from supported material,
// shrunk back, stripped from `last`, and expanded back. It is then prevented
// from intruding deeper into `last` than the explicit anchor overlap.
// Finally, add the allowed anchor band from `last` then remove the
// narrow hole-side wall contact, which must remain unbridgeable.
last = diff_ex(last, unsupported_filtered, ApplySafetyOffset::Yes);
ExPolygons bridgeable_filtered;
for (ExPolygon& poly : bridgeable) {
poly.simplify(perimeter_spacing, &bridgeable_simplified);
poly.simplify(perimeter_spacing, &bridgeable_filtered);
}
bridgeable_simplified = offset2_ex(bridgeable_simplified, -ext_perimeter_width, ext_perimeter_width);
//bridgeable_simplified = intersection_ex(bridgeable_simplified, unsupported_filtered);
//offset by perimeter spacing because the simplify may have reduced it a bit.
//it's not dangerous as it will be intersected by 'unsupported' later
//FIXME: add overlap in this->fill_surfaces->append
//FIXME: it overlap inside unsuppported not-bridgeable area!
bridgeable_filtered = opening_ex(bridgeable_filtered, ext_perimeter_width);
//bridgeable_simplified = offset2_ex(bridgeable_simplified, (double)-perimeter_spacing, (double)perimeter_spacing * 2);
//ExPolygons unbridgeable = offset_ex(diff_ex(unsupported, bridgeable_simplified), perimeter_spacing * 3 / 2);
//ExPolygons unbridgeable = intersection_ex(unsupported, diff_ex(unsupported_filtered, offset_ex(bridgeable_simplified, ext_perimeter_width / 2)));
//unbridgeable = offset2_ex(unbridgeable, -ext_perimeter_width, ext_perimeter_width);
// Get rid of coarseness of the resulted bridgeable area by using the original supported area as reference.
// This is to avoid keeping tiny bridgeable areas that are far from the supported area, or protrude into it.
bridgeable_filtered = union_ex(offset_ex(last, perimeter_spacing), bridgeable_filtered);
bridgeable_filtered = offset_ex(bridgeable_filtered, -perimeter_spacing);
bridgeable_filtered = diff_ex(bridgeable_filtered, last, ApplySafetyOffset::Yes);
bridgeable_filtered = opening_ex(bridgeable_filtered, perimeter_spacing); // filter noise from the diff_ex
bridgeable_filtered = offset_ex(bridgeable_filtered, perimeter_spacing); // restore the size to the original bridgeable area
// Safety measure: Keep the bridge mask from intruding deeper into the
// supported anchor region (`last`) than the explicit anchor overlap.
bridgeable_filtered = diff_ex(bridgeable_filtered, offset_ex(last, -bridge_anchor_offset));
// if (this->config->counterbore_hole_bridging.value == chbBridges) {
ExPolygons unbridgeable = unsupported_filtered;
for (ExPolygon& expol : unbridgeable)
expol.holes.clear();
unbridgeable = diff_ex(unbridgeable, bridgeable_simplified);
unbridgeable = offset2_ex(unbridgeable, -ext_perimeter_width * 2, ext_perimeter_width * 2);
ExPolygons bridges_temp = offset2_ex(intersection_ex(last, diff_ex(unsupported_filtered, unbridgeable), ApplySafetyOffset::Yes), -ext_perimeter_width / 4, ext_perimeter_width / 4);
//remove the overhangs section from the surface polygons
ExPolygons reference = last;
last = diff_ex(last, unsupported_filtered);
//ExPolygons no_bridge = diff_ex(offset_ex(unbridgeable, ext_perimeter_width * 3 / 2), last);
//bridges_temp = diff_ex(bridges_temp, no_bridge);
coordf_t offset_to_do = bridged_infill_margin;
bool first = true;
unbridgeable = diff_ex(unbridgeable, offset_ex(bridges_temp, ext_perimeter_width));
while (offset_to_do > ext_perimeter_width * 1.5) {
unbridgeable = offset2_ex(unbridgeable, -ext_perimeter_width / 4, ext_perimeter_width * 2.25, ClipperLib::jtSquare);
bridges_temp = diff_ex(bridges_temp, unbridgeable);
bridges_temp = offset_ex(bridges_temp, ext_perimeter_width, ClipperLib::jtMiter, 6.);
unbridgeable = diff_ex(unbridgeable, offset_ex(bridges_temp, ext_perimeter_width));
offset_to_do -= ext_perimeter_width;
first = false;
}
unbridgeable = offset_ex(unbridgeable, ext_perimeter_width + offset_to_do, ClipperLib::jtSquare);
bridges_temp = diff_ex(bridges_temp, unbridgeable);
unsupported_filtered = offset_ex(bridges_temp, offset_to_do);
unsupported_filtered = intersection_ex(unsupported_filtered, reference);
// Normalize anchor size for partial bridges:
// derive the bridge core first, then add a fixed overlap into support.
const coordf_t anchor_overlap = bridged_infill_margin;
ExPolygons bridge_core = diff_ex(unsupported_filtered, support, ApplySafetyOffset::Yes);
if (bridge_core.empty()) {
bridge_core = unsupported_filtered;
}
ExPolygons anchor_overlap_area = intersection_ex(
offset_ex(bridge_core, anchor_overlap),
support,
ApplySafetyOffset::Yes);
unsupported_filtered = union_ex(bridge_core, anchor_overlap_area);
unsupported_filtered = intersection_ex(unsupported_filtered, reference);
// } else {
// ExPolygons unbridgeable = intersection_ex(unsupported, diff_ex(unsupported_filtered, offset_ex(bridgeable_simplified, ext_perimeter_width / 2)));
// unbridgeable = offset2_ex(unbridgeable, -ext_perimeter_width, ext_perimeter_width);
// unsupported_filtered = unbridgeable;
// ////put the bridge area inside the unsupported_filtered variable
// //unsupported_filtered = intersection_ex(last,
// // diff_ex(
// // offset_ex(bridgeable_simplified, (double)perimeter_spacing / 2),
// // unbridgeable
// // )
// // );
// }
} else {
unsupported_filtered.clear();
ExPolygons bridge_anchor_areas = intersection_ex(last, offset_ex(unsupported_filtered, bridge_anchor_offset));
unsupported_filtered = union_ex(bridgeable_filtered, bridge_anchor_areas); // add bridge anchor
unsupported_filtered = opening_ex(unsupported_filtered, bridge_anchor_offset); // remove anchor area from hole-side walls, it must remain unbridgeable
// TODO: Fix the case with thin outer walls around the bridge (1~2 walls) where classic wall
// might generate two walls in a tiny space or non at all if "Detect thin walls" is not activated
}
} else {
unsupported_filtered.clear();
@@ -2146,7 +2114,7 @@ void PerimeterGenerator::process_arachne()
// We consider overhang any part where the entire nozzle diameter is not supported by the
// lower layer, so we take lower slices and offset them by half the nozzle diameter used
// in the current layer
double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->wall_filament - 1);
double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->outer_wall_filament_id - 1);
m_lower_slices_polygons = offset(*this->lower_slices, float(scale_(+nozzle_diameter / 2)));
}
@@ -2579,7 +2547,7 @@ bool PerimeterGeneratorLoop::is_internal_contour() const
std::vector<Polygons> PerimeterGenerator::generate_lower_polygons_series(float width)
{
float nozzle_diameter = print_config->nozzle_diameter.get_at(config->wall_filament - 1);
float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1);
float start_offset = -0.5 * width;
float end_offset = 0.5 * nozzle_diameter;
+3
View File
@@ -117,6 +117,7 @@ public:
const PrintObjectConfig* object_config,
const PrintConfig* print_config,
const bool spiral_mode,
const double model_rotation_rad,
// Output:
// Loops with the external thin walls
ExtrusionEntityCollection* loops,
@@ -132,6 +133,7 @@ public:
config(config), object_config(object_config), print_config(print_config),
m_spiral_vase(spiral_mode),
m_scaled_resolution(scaled<double>(print_config->resolution.value > EPSILON ? print_config->resolution.value : EPSILON)),
m_model_rotation_rad(model_rotation_rad),
loops(loops), gap_fill(gap_fill), fill_surfaces(fill_surfaces), fill_no_overlap(fill_no_overlap),
m_ext_mm3_per_mm(-1), m_mm3_per_mm(-1), m_mm3_per_mm_overhang(-1), m_ext_mm3_per_mm_smaller_width(-1)
{}
@@ -157,6 +159,7 @@ private:
private:
bool m_spiral_vase;
double m_scaled_resolution;
double m_model_rotation_rad;
double m_ext_mm3_per_mm;
double m_mm3_per_mm;
double m_mm3_per_mm_overhang;
+12 -4
View File
@@ -1005,6 +1005,9 @@ static std::vector<std::string> s_Preset_print_options{
"lateral_lattice_angle_1",
"lateral_lattice_angle_2",
"infill_overhang_angle",
"lightning_overhang_angle",
"lightning_prune_angle",
"lightning_straightening_angle",
"top_surface_pattern",
"bottom_surface_pattern",
"infill_direction",
@@ -1069,10 +1072,13 @@ static std::vector<std::string> s_Preset_print_options{
"print_order",
"support_remove_small_overhang",
"filename_format",
"wall_filament",
"outer_wall_filament_id",
"inner_wall_filament_id",
"support_bottom_z_distance",
"sparse_infill_filament",
"solid_infill_filament",
"sparse_infill_filament_id",
"internal_solid_filament_id",
"top_surface_filament_id",
"bottom_surface_filament_id",
"support_filament",
"support_interface_filament",
"support_interface_not_for_body",
@@ -1094,6 +1100,7 @@ static std::vector<std::string> s_Preset_print_options{
"infill_wall_overlap",
"top_bottom_infill_wall_overlap",
"bridge_flow",
"bridge_line_width",
"internal_bridge_flow",
"elefant_foot_compensation",
"elefant_foot_compensation_layers",
@@ -1158,6 +1165,7 @@ static std::vector<std::string> s_Preset_print_options{
"small_perimeter_threshold",
"bridge_angle",
"internal_bridge_angle",
"relative_bridge_angle",
"filter_out_gap_fill",
"travel_acceleration",
"inner_wall_acceleration",
@@ -1318,7 +1326,7 @@ static std::vector<std::string> s_Preset_machine_limits_options {
static std::vector<std::string> s_Preset_printer_options {
"printer_technology",
"printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor",
"printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor",
"fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
"single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode",
"printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type",
+16 -5
View File
@@ -2871,6 +2871,14 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p
if (use_default_nozzle_volume_type) {
project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")->values = current_printer.config.option<ConfigOptionEnumsGeneric>("default_nozzle_volume_type")->values;
} else {
// Orca: make sure `nozzle_volume_type` not shorter than `default_nozzle_volume_type`, otherwise we got array out of bound access
// later in `Tab::switch_excluder`
auto& opt = project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")->values;
const auto& opt_default = current_printer.config.option<ConfigOptionEnumsGeneric>("default_nozzle_volume_type")->values;
while (opt.size() < opt_default.size()) {
opt.emplace_back(opt_default[opt.size()]);
}
}
// Parse the initial physical printer name.
@@ -4095,13 +4103,16 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio
opt->value = boost::algorithm::clamp<int>(opt->value, 0, int(num_filaments));
}
static const char* keys_1based[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament"};
for (size_t i = 0; i < sizeof(keys_1based) / sizeof(keys_1based[0]); ++ i) {
std::string key = std::string(keys_1based[i]);
static const char* keys_with_default[] = {
"outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_filament_id",
"internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id"
};
for (size_t i = 0; i < sizeof(keys_with_default) / sizeof(keys_with_default[0]); ++ i) {
std::string key = std::string(keys_with_default[i]);
auto *opt = dynamic_cast<ConfigOptionInt*>(out.option(key, false));
assert(opt != nullptr);
if(opt->value < 1 || opt->value > int(num_filaments))
opt->value = 1;
if(opt->value < 0 || opt->value > int(num_filaments))
opt->value = 0;
}
out.option<ConfigOptionString >("print_settings_id", true)->value = this->prints.get_selected_preset_name();
out.option<ConfigOptionStrings>("filament_settings_id", true)->values = this->filament_presets;
+82 -30
View File
@@ -1254,10 +1254,6 @@ StringObjectException Print::check_multi_filament_valid(const Print& print)
return ret;
}
// Orca: this g92e0 regex is used copied from PrusaSlicer
// Matches "G92 E0" with various forms of writing the zero and with an optional comment.
boost::regex regex_g92e0 { "^[ \\t]*[gG]92[ \\t]*[eE](0(\\.0*)?|\\.0+)[ \\t]*(;.*)?$" };
// Precondition: Print::validate() requires the Print::apply() to be called its invocation.
//BBS: refine seq-print validation logic.....FIXME:StringObjectException *warning can only contain one warning, but there might be many warnings, need a vector<StringObjectException>
StringObjectException Print::validate(StringObjectException *warning, Polygons* collison_polygons, std::vector<std::pair<Polygon, float>>* height_polygons) const
@@ -1544,12 +1540,12 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
auto validate_extrusion_width = [min_nozzle_diameter, max_nozzle_diameter](const ConfigBase &config, const char *opt_key, double layer_height, std::string &err_msg) -> bool {
double extrusion_width_min = config.get_abs_value(opt_key, min_nozzle_diameter);
double extrusion_width_max = config.get_abs_value(opt_key, max_nozzle_diameter);
if (extrusion_width_min == 0) {
// Default "auto-generated" extrusion width is always valid.
} else if (extrusion_width_min <= layer_height) {
err_msg = L("Too small line width");
return false;
} else if (extrusion_width_max > max_nozzle_diameter * MAX_LINE_WIDTH_MULTIPLIER) {
if (extrusion_width_min == 0) {
// Default "auto-generated" extrusion width is always valid.
} else if (extrusion_width_min <= layer_height) {
err_msg = L("Too small line width");
return false;
} else if (extrusion_width_max > max_nozzle_diameter * MAX_LINE_WIDTH_MULTIPLIER) {
err_msg = L("Too large line width");
return false;
}
@@ -1671,30 +1667,83 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
for (const PrintRegion &region : object->all_regions())
if (!validate_extrusion_width(region.config(), opt_key, layer_height, err_msg))
return {err_msg, object, opt_key};
const bool allow_thin_bridge_width = object->config().thick_bridges && object->config().thick_internal_bridges;
for (const PrintRegion &region : object->all_regions()) {
const auto &bridge_width_opt = region.config().bridge_line_width;
for (FlowRole bridge_role : { frPerimeter, frInfill, frSolidInfill, frTopSolidInfill }) {
const double nozzle_diameter = m_config.nozzle_diameter.get_at(region.extruder(bridge_role) - 1);
const double bridge_width = bridge_width_opt.get_abs_value(nozzle_diameter);
if (bridge_width <= 0.)
continue;
if (bridge_width > nozzle_diameter) {
err_msg = L("Bridge line width must not exceed nozzle diameter");
return { err_msg, object, "bridge_line_width" };
}
if (!allow_thin_bridge_width && bridge_width <= layer_height) {
err_msg = L("Too small line width");
return { err_msg, object, "bridge_line_width" };
}
}
}
}
}
// Orca: G92 E0 is not supported when using absolute extruder addressing
// This check is copied from PrusaSlicer, the original author is Vojtech Bubnik
if(!is_BBL_printer()) {
bool before_layer_gcode_resets_extruder =
boost::regex_search(m_config.before_layer_change_gcode.value, regex_g92e0);
bool layer_gcode_resets_extruder = boost::regex_search(m_config.layer_change_gcode.value, regex_g92e0);
if (m_config.use_relative_e_distances) {
// See GH issues #6336 #5073
if ((m_config.gcode_flavor == gcfMarlinLegacy || m_config.gcode_flavor == gcfMarlinFirmware) &&
!before_layer_gcode_resets_extruder && !layer_gcode_resets_extruder)
return {L("Relative extruder addressing requires resetting the extruder position at each layer to "
"prevent loss of floating point accuracy. Add \"G92 E0\" to layer_gcode."),
nullptr, "before_layer_change_gcode"};
} else if (before_layer_gcode_resets_extruder)
return {L("\"G92 E0\" was found in before_layer_gcode, which is incompatible with absolute extruder "
// This check is modified from PrusaSlicer, the original author is Vojtech Bubnik
// Orca: case‑sensitive match for exactly "G92 E0" (uppercase G and E only)
// because gcode is case sensitive and G92 e0 satisfies the regex but causes a slicing error
// https://github.com/OrcaSlicer/OrcaSlicer/issues/13927
// Matches any case of "G92 E0" (original pattern)
static const boost::regex regex_g92e0 {
"^[ \\t]*[gG]92[ \\t]*[eE](0(\\.0*)?|\\.0+)[ \\t]*(;.*)?$"
};
// Matches only the exact uppercase "G92 E0"
static const boost::regex regex_g92e0_correct {
"^[ \\t]*G92[ \\t]*E(0(\\.0*)?|\\.0+)[ \\t]*(;.*)?$"
};
const bool before_has_g92_any = boost::regex_search(
m_config.before_layer_change_gcode.value, regex_g92e0);
const bool layer_has_g92_any = boost::regex_search(
m_config.layer_change_gcode.value, regex_g92e0);
if (m_config.use_relative_e_distances) {
// Relative mode: "G92 E0" is required to reset extruder position.
const bool before_has_g92_exact = boost::regex_search(
m_config.before_layer_change_gcode.value, regex_g92e0_correct);
const bool layer_has_g92_exact = boost::regex_search(
m_config.layer_change_gcode.value, regex_g92e0_correct);
// Wrong case found?
if (before_has_g92_any && !before_has_g92_exact)
return {L("\"G92 E0\" was found in before_layer_change_gcode, but the G or E are not uppercase. "
"Please change them to the exact uppercase \"G92 E0\"."),
nullptr, "before_layer_change_gcode"};
if (layer_has_g92_any && !layer_has_g92_exact)
return {L("\"G92 E0\" was found in layer_change_gcode, but the G or E are not uppercase. "
"Please change them to the exact uppercase \"G92 E0\"."),
nullptr, "layer_change_gcode"};
// Only Marlin flavours need the reset; BBL printers do not.
if ((m_config.gcode_flavor == gcfMarlinLegacy || m_config.gcode_flavor == gcfMarlinFirmware) &&
!is_BBL_printer() &&
!before_has_g92_exact && !layer_has_g92_exact)
return {L("Relative extruder addressing requires resetting the extruder position at each layer to "
"prevent loss of floating point accuracy. Add \"G92 E0\" to layer_gcode."),
nullptr, "before_layer_change_gcode"};
} else {
// Absolute mode: any occurrence of "G92 E0" is incompatible.
if (before_has_g92_any)
return {L("\"G92 E0\" was found in before_layer_change_gcode, which is incompatible with absolute extruder "
"addressing."),
nullptr, "before_layer_change_gcode"};
else if (layer_gcode_resets_extruder)
return {L("\"G92 E0\" was found in layer_gcode, which is incompatible with absolute extruder addressing."),
if (layer_has_g92_any)
return {L("\"G92 E0\" was found in layer_change_gcode, which is incompatible with absolute extruder "
"addressing."),
nullptr, "layer_change_gcode"};
}
}
const ConfigOptionDef* bed_type_def = print_config_def.get("curr_bed_type");
assert(bed_type_def != nullptr);
@@ -1972,7 +2021,7 @@ Flow Print::brim_flow() const
frPerimeter,
// Flow::new_from_config_width takes care of the percent to value substitution
width,
(float)m_config.nozzle_diameter.get_at(m_print_regions.front()->config().wall_filament-1),
(float)m_config.nozzle_diameter.get_at(m_print_regions.front()->config().outer_wall_filament_id-1),
(float)this->skirt_first_layer_height());
}
@@ -3609,9 +3658,12 @@ DynamicConfig PrintStatistics::config() const
config.set_key_value("total_cost", new ConfigOptionFloat(this->total_cost));
config.set_key_value("total_toolchanges", new ConfigOptionInt(this->total_toolchanges));
config.set_key_value("total_weight", new ConfigOptionFloat(this->total_weight));
config.set_key_value("extruded_weight_total", new ConfigOptionFloat(this->total_weight));
config.set_key_value("extruded_volume_total", new ConfigOptionFloat(this->total_extruded_volume));
config.set_key_value("total_wipe_tower_cost", new ConfigOptionFloat(this->total_wipe_tower_cost));
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)));
return config;
}
@@ -3620,8 +3672,8 @@ DynamicConfig PrintStatistics::placeholders()
DynamicConfig config;
for (const std::string key : {
"print_time", "normal_print_time", "silent_print_time",
"used_filament", "extruded_volume", "total_cost", "total_weight",
"initial_tool", "total_toolchanges", "total_wipe_tower_cost", "total_wipe_tower_filament"})
"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"})
config.set_key_value(key, new ConfigOptionString(std::string("{") + key + "}"));
return config;
}
+16 -6
View File
@@ -815,9 +815,12 @@ bool verify_update_print_object_regions(
for (const PrintObjectRegions::PaintedRegion &region : layer_range.painted_regions) {
const PrintObjectRegions::VolumeRegion &parent_region = layer_range.volume_regions[region.parent];
PrintRegionConfig cfg = parent_region.region->config();
cfg.wall_filament.value = region.extruder_id;
cfg.solid_infill_filament.value = region.extruder_id;
cfg.sparse_infill_filament.value = region.extruder_id;
cfg.outer_wall_filament_id.value = region.extruder_id;
cfg.inner_wall_filament_id.value = region.extruder_id;
cfg.internal_solid_filament_id.value = region.extruder_id;
cfg.top_surface_filament_id.value = region.extruder_id;
cfg.bottom_surface_filament_id.value = region.extruder_id;
cfg.sparse_infill_filament_id.value = region.extruder_id;
if (cfg != region.region->config()) {
// Region configuration changed.
if (print_region_ref_cnt(*region.region) == 0) {
@@ -1060,9 +1063,12 @@ static PrintObjectRegions* generate_print_object_regions(
if (const PrintObjectRegions::VolumeRegion &parent_region = layer_range.volume_regions[parent_region_id];
parent_region.model_volume->is_model_part() || parent_region.model_volume->is_modifier()) {
PrintRegionConfig cfg = parent_region.region->config();
cfg.wall_filament.value = painted_extruder_id;
cfg.solid_infill_filament.value = painted_extruder_id;
cfg.sparse_infill_filament.value = painted_extruder_id;
cfg.outer_wall_filament_id.value = painted_extruder_id;
cfg.inner_wall_filament_id.value = painted_extruder_id;
cfg.internal_solid_filament_id.value = painted_extruder_id;
cfg.top_surface_filament_id.value = painted_extruder_id;
cfg.bottom_surface_filament_id.value = painted_extruder_id;
cfg.sparse_infill_filament_id.value = painted_extruder_id;
layer_range.painted_regions.push_back({ painted_extruder_id, parent_region_id, get_create_region(std::move(cfg))});
}
// Sort the regions by parent region::print_object_region_id() and extruder_id to help the slicing algorithm when applying MM segmentation.
@@ -1254,6 +1260,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: found full_config_diff changed.")%__LINE__;
update_apply_status(this->invalidate_step(psGCodeExport));
m_placeholder_parser.clear_config();
// clear_config() wiped the constructor-set "version"; restore it for custom G-code.
m_placeholder_parser.set("version", std::string(SoftFever_VERSION));
// Set the profile aliases for the PrintBase::output_filename()
m_placeholder_parser.set("print_preset", new_full_config.option("print_settings_id")->clone());
m_placeholder_parser.set("filament_preset", new_full_config.option("filament_settings_id")->clone());
@@ -1630,6 +1638,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: full_config_diff previous empty, need to apply now.")%__LINE__;
m_placeholder_parser.clear_config();
// clear_config() wiped the constructor-set "version"; restore it for custom G-code.
m_placeholder_parser.set("version", std::string(SoftFever_VERSION));
// Set the profile aliases for the PrintBase::output_filename()
m_placeholder_parser.set("print_preset", new_full_config.option("print_settings_id")->clone());
m_placeholder_parser.set("filament_preset", new_full_config.option("filament_settings_id")->clone());
+297 -73
View File
@@ -148,7 +148,9 @@ static t_config_enum_values s_keys_map_PrintHostType {
{ "obico", htObico },
{ "flashforge", htFlashforge },
{ "simplyprint", htSimplyPrint },
{ "elegoolink", htElegooLink }
{ "elegoolink", htElegooLink },
{ "3dprinteros", ht3DPrinterOS },
{ "moonraker", htMoonraker }
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintHostType)
@@ -693,6 +695,26 @@ void PrintConfigDef::init_common_params()
def->gui_type = ConfigOptionDef::GUIType::one_string;
def->set_default_value(new ConfigOptionPointsGroups{});
def = this->add("support_parallel_printheads", coBool);
def->label = L("Support parallel printheads");
def->tooltip = L("Enable printer settings for machines that can use multiple printheads in parallel.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool{false});
def = this->add("parallel_printheads_count", coInt);
def->label = L("Parallel printheads count");
def->tooltip = L("Set the number of parallel printheads for machines like OrangeStorm Giga printer.");
def->mode = comAdvanced;
def->min = 1;
def->max = 4;
def->set_default_value(new ConfigOptionInt{1});
def = this->add("parallel_printheads_bed_exclude_areas", coStrings);
def->label = L("Parallel printheads bed exclude areas");
def->tooltip = L("Ordered list of bed exclude areas by parallel printhead count. Item 1 applies to one printhead, item 2 to two printheads, and so on. Leave an item empty for no excluded area.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());
//BBS: add "bed_exclude_area"
def = this->add("bed_exclude_area", coPoints);
def->label = L("Bed exclude area");
@@ -1221,11 +1243,16 @@ void PrintConfigDef::init_fff_params()
def->label = L("External bridge infill direction");
def->category = L("Strength");
// xgettext:no-c-format, no-boost-format
def->tooltip = L("Bridging angle override. If left to zero, the bridging angle will be calculated "
"automatically. Otherwise the provided angle will be used for external bridges. "
"Use 180° for zero angle.");
def->tooltip = L("External Bridging angle override.\n"
"If left to zero, the bridging angle will be calculated automatically for each specific bridge.\n"
"Otherwise the provided angle will be used according to:\n"
" - The absolute coordinates\n"
" - The absolute coordinates + Model rotation: If Align infill direction to model is enabled\n"
" - The optimal automatic angle + this value: If 'Relative Bridge Angle' is enabled\n\n"
"Use 180° for zero absolute angle.");
def->sidetext = u8"°"; // degrees, don't need translation
def->min = 0;
def->max = 180;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.));
@@ -1233,58 +1260,97 @@ void PrintConfigDef::init_fff_params()
def = this->add("internal_bridge_angle", coFloat);
def->label = L("Internal bridge infill direction");
def->category = L("Strength");
def->tooltip = L("Internal bridging angle override. If left to zero, the bridging angle will be calculated "
"automatically. Otherwise the provided angle will be used for internal bridges. "
"Use 180° for zero angle.\n\nIt is recommended to leave it at 0 unless there is a specific model need not to.");
def->tooltip = L("Internal Bridging angle override.\n"
"If left to zero, the bridging angle will be calculated automatically for each specific bridge.\n"
"Otherwise the provided angle will be used according to:\n"
" - The absolute coordinates\n"
" - The absolute coordinates + Model rotation: If Align infill direction to model is enabled\n"
" - The optimal automatic angle + this value: If 'Relative Bridge Angle' is enabled\n\n"
"Use 180° for zero absolute angle.");
def->sidetext = u8"°"; // degrees, don't need translation
def->min = 0;
def->max = 180;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.));
// ORCA: Relative bridge angle
def = this->add("relative_bridge_angle", coBool);
def->label = L("Relative bridge angle");
def->category = L("Strength");
def->tooltip = L("When enabled, the bridge angle values are added to the automatically calculated bridge direction instead of overriding it.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("bridge_density", coPercent);
def->label = L("External bridge density");
def->category = L("Strength");
def->tooltip = L("Controls the density (spacing) of external bridge lines. Default is 100%.\n\n"
"Lower density external bridges can help improve reliability as there is more space for air to circulate "
"around the extruded bridge, improving its cooling speed. Minimum is 10%.\n\n"
"Higher densities can produce smoother bridge surfaces, as overlapping lines provide "
"additional support during printing. Maximum is 120%.\n"
"Note: Bridge density that is too high can cause warping or overextrusion.");
def->tooltip = L("Controls the density (spacing) of external bridge lines. Default is 100%.\n"
"Theoretically, 100% means a solid bridge, but due to the tendency of bridge extrusions to sag, 100% may not be sufficient.\n\n"
"- Higher than 100% density (Recommended Max 125%):\n"
" - Pros: Produces smoother bridge surfaces, as overlapping lines provide additional support during printing.\n"
" - Cons: Can cause overextrusion, which may reduce lower and upper surface quality and increase the risk of warping.\n\n"
"- Lower than 100% density (Min 10%):\n"
" - Pros: Can create a string-like first layer. Faster and with better cooling because there is more space for air to circulate around the extruded bridge.\n"
" - Cons: May lead to sagging and poorer surface finish.\n\n"
"Recommended range: Minimum 10% - Maximum 125%.");
def->sidetext = "%";
def->min = 10;
def->max = 120;
def->max = 125;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionPercent(100));
def = this->add("internal_bridge_density", coPercent);
def->label = L("Internal bridge density");
def->category = L("Strength");
def->tooltip = L("Controls the density (spacing) of internal bridge lines. 100% means solid bridge. Default is 100%.\n\n"
"Lower density internal bridges can help reduce top surface pillowing and improve internal bridge reliability as there is more space for "
"air to circulate around the extruded bridge, improving its cooling speed.\n\n"
"This option works particularly well when combined with the second internal bridge over infill option, "
"further improving internal bridging structure before solid infill is extruded.");
def->tooltip = L("Controls the density (spacing) of internal bridge lines. Default is 100%. 100% means a solid internal bridge.\n\n"
"Internal bridges act as intermediate support between sparse infill and top solid infill and can strongly affect top surface quality.\n\n"
"- Higher than 100% density (Recommended Max 125%):\n"
" - Pros: Improves internal bridge strength and support under top layers, reducing sagging and improving top-surface finish.\n"
" - Cons: Increases material use and print time; excessive density may cause overextrusion and internal stresses.\n\n"
"- Lower than 100% density (Min 10%):\n"
" - Pros: Can reduce pillowing and improve cooling (more airflow through the bridge), and may speed up printing.\n"
" - Cons: May reduce internal support, increasing the risk of sagging and top surface defects.\n\n"
"This option works particularly well when combined with the second internal bridge over infill option to improve bridging further before solid infill is extruded.");
def->sidetext = "%";
def->min = 10;
def->max = 100;
def->max = 125;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionPercent(100));
def = this->add("bridge_flow", coFloat);
def->label = L("Bridge flow ratio");
def->category = L("Quality");
def->tooltip = L("Decrease this value slightly (for example 0.9) to reduce the amount of material for bridge, to improve sag.\n\n"
def->tooltip = L("This value governs the thickness of the external (visible) bridge layer.\n"
"Values above 1.0: Increase the amount of material while maintaining line spacing. This can improve line contact and strength.\n"
"Values below 1.0: Reduce the amount of material while adjusting line spacing to maintain contact. This can improve sagging.\n\n"
"The actual bridge flow used is calculated by multiplying this value with the filament flow ratio, and if set, the object's flow ratio.");
def->min = 0;
def->max = 2.0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(1));
def = this->add("bridge_line_width", coFloatOrPercent);
def->label = L("Bridge");
def->category = L("Quality");
def->tooltip = L("Bridge line width is expressed either as an absolute value or as a percentage of the active nozzle diameter (percentages are computed from the nozzle diameter).\n"
"Recommended to use with a higher Bridge density or Bridge flow ratio.\n\n"
"The maximum value is 100% or the nozzle diameter.\n"
"If set to 0, the line width will match the Internal solid infill width.");
def->sidetext = L("mm or %");
def->ratio_over = "nozzle_diameter";
def->min = 0;
def->max = 100;
def->max_literal = 10;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(100., true));
def = this->add("internal_bridge_flow", coFloat);
def->label = L("Internal bridge flow ratio");
def->category = L("Quality");
def->tooltip = L("This value governs the thickness of the internal bridge layer. This is the first layer over sparse infill. Decrease this value slightly (for example 0.9) to improve surface quality over sparse infill."
"\n\nThe actual internal bridge flow used is calculated by multiplying this value with the bridge flow ratio, the filament flow ratio, and if set, the object's flow ratio.");
def->tooltip = L("This value governs the thickness of the internal bridge layer. This is the first layer over sparse infill so increasing it may increase strength and upper layer quality.\n"
"Values above 1.0: Increase the amount of material while maintaining line spacing. This can improve line contact and strength.\n"
"Values below 1.0: Reduce the amount of material while adjusting line spacing to maintain contact. This can improve sagging.\n\n"
"The actual bridge flow used is calculated by multiplying this value with the filament flow ratio, and if set, the object's flow ratio.");
def->min = 0;
def->max = 2.0;
def->mode = comAdvanced;
@@ -1515,18 +1581,24 @@ void PrintConfigDef::init_fff_params()
def->label = L("Slow down for curled perimeters");
def->category = L("Speed");
// xgettext:no-c-format, no-boost-format
def->tooltip = L("Enable this option to slow down printing in areas where perimeters may have curled upwards. "
def->tooltip = L("Enable this option to slow down printing in areas where perimeters may have curled upwards.\n"
"For example, additional slowdown will be applied when printing overhangs on sharp corners like the "
"front of the Benchy hull, reducing curling which compounds over multiple layers.\n\n"
"It is generally recommended to have this option switched on unless your printer cooling is powerful enough or the "
"print speed slow enough that perimeter curling does not happen. If printing with a high external perimeter speed, "
"this parameter may introduce slight artifacts when slowing down due to the large variance in print speeds. "
"If you notice artifacts, ensure your pressure advance is tuned correctly.\n\n"
"print speed is slow enough that perimeter curling does not happen. \n"
"If printing with a high external perimeter speed, this parameter may introduce wall artifacts when slowing down, "
"due to the potentially large variance in print speeds causing the extruder to be unable to keep up with the requested flow change.\n"
"Root cause of these artifacts is most likely PA tuning being slightly off, especially when combined "
"with a high PA smooth time.\n\n"
"Recommendations when enabling this option:\n"
"1. Reduce Pressure Advance smooth time to 0.015 - 0.02 so the extruder reacts quickly to the speed changes.\n"
"2. Increase the minimum print speeds to limit the magnitude of the slowdown and reduce the variance between fast and slow segments.\n"
"3. If artifacts still appear, enable Extrusion Rate Smoothing (ERS) to further smooth the flow transitions.\n\n"
"Note: When this option is enabled, overhang perimeters are treated like overhangs, meaning the overhang speed is "
"applied even if the overhanging perimeter is part of a bridge. For example, when the perimeters are 100% overhanging"
", with no wall supporting them from underneath, the 100% overhang speed will be applied.");
"applied even if the overhanging perimeter is part of a bridge.\n"
"For example, when the perimeters are 100% overhanging, with no wall supporting them from underneath, the 100% overhang speed will be applied.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool{ true });
def->set_default_value(new ConfigOptionBool{ false });
def = this->add("overhang_1_4_speed", coFloatOrPercent);
def->label = "10%";
@@ -1862,16 +1934,18 @@ void PrintConfigDef::init_fff_params()
def = this->add("thick_bridges", coBool);
def->label = L("Thick external bridges");
def->category = L("Quality");
def->tooltip = L("If enabled, bridges are more reliable, can bridge longer distances, but may look worse. "
"If disabled, bridges look better but are reliable just for shorter bridged distances.");
def->tooltip = L("If enabled, bridge extrusion uses a line height equal to the nozzle diameter.\n"
"This increases bridge strength and reliability, allowing longer spans, but may worsen appearance.\n"
"If disabled, bridges may look better but are generally reliable only for shorter spans.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("thick_internal_bridges", coBool);
def->label = L("Thick internal bridges");
def->category = L("Quality");
def->tooltip = L("If enabled, thick internal bridges will be used. It's usually recommended to have this feature turned on. However, "
"consider turning it off if you are using large nozzles.");
def->tooltip = L("If enabled, internal bridge extrusion uses a line height equal to the nozzle diameter.\n"
"This increases internal bridge strength and reliability when printed over sparse infill, but may worsen appearance.\n"
"If disabled, internal bridges may look better but can be less reliable over sparse infill.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(true));
@@ -2898,7 +2972,8 @@ void PrintConfigDef::init_fff_params()
def = this->add("align_infill_direction_to_model", coBool);
def->label = L("Align infill direction to model");
def->category = L("Strength");
def->tooltip = L("Aligns infill and surface fill directions to follow the model's orientation on the build plate. When enabled, fill directions rotate with the model to maintain optimal strength characteristics.");
def->tooltip = L("Aligns infill, bridge, ironing and surface fill directions to follow the model's orientation on the build plate.\n"
"When enabled, directions rotate with the model to maintain optimal strength characteristics.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
@@ -3021,6 +3096,37 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(60));
def = this->add("lightning_overhang_angle", coFloat);
def->label = L("Lightning overhang angle");
def->category = L("Strength");
def->tooltip = L("Maximum overhang angle for Lightning infill support propagation.");
def->sidetext = u8"°"; // degrees, don't need translation
def->min = 5;
def->max = 85;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloat(45));
def = this->add("lightning_prune_angle", coFloat);
def->label = L("Prune angle");
def->category = L("Strength");
def->tooltip = L("Controls how aggressively short or unsupported Lightning branches are pruned.\n"
"This angle is converted internally to a per-layer distance.");
def->sidetext = u8"°"; // degrees, don't need translation
def->min = 5;
def->max = 85;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloat(45));
def = this->add("lightning_straightening_angle", coFloat);
def->label = L("Straightening angle");
def->category = L("Strength");
def->tooltip = L("Maximum straightening angle used to simplify Lightning branches.");
def->sidetext = u8"°"; // degrees, don't need translation
def->min = 5;
def->max = 85;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloat(45));
auto def_infill_anchor_min = def = this->add("infill_anchor", coFloatOrPercent);
def->label = L("Sparse infill anchor length");
def->category = L("Strength");
@@ -4011,14 +4117,14 @@ void PrintConfigDef::init_fff_params()
def->gui_type = ConfigOptionDef::GUIType::one_string;
def->set_default_value(new ConfigOptionPoints());
def = this->add("sparse_infill_filament", coInt);
def = this->add("sparse_infill_filament_id", coInt);
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
def->label = L("Infill");
def->category = L("Extruders");
def->tooltip = L("Filament to print internal sparse infill.");
def->min = 1;
def->tooltip = L("Filament to print internal sparse infill.\n\"Default\" uses the active object/part filament.");
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(1));
def->set_default_value(new ConfigOptionInt(0));
def = this->add("sparse_infill_line_width", coFloatOrPercent);
def->label = L("Sparse infill");
@@ -4756,6 +4862,8 @@ void PrintConfigDef::init_fff_params()
def->enum_values.push_back("flashforge");
def->enum_values.push_back("simplyprint");
def->enum_values.push_back("elegoolink");
def->enum_values.push_back("3dprinteros");
def->enum_values.push_back("moonraker");
def->enum_labels.push_back("PrusaLink");
def->enum_labels.push_back("PrusaConnect");
def->enum_labels.push_back("Octo/Klipper");
@@ -4770,6 +4878,8 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back("Flashforge");
def->enum_labels.push_back("SimplyPrint");
def->enum_labels.push_back("Elegoo Link");
def->enum_labels.push_back("3DPrinterOS");
def->enum_labels.push_back("Moonraker (Klipper)");
def->mode = comAdvanced;
def->cli = ConfigOptionDef::nocli;
def->set_default_value(new ConfigOptionEnum<PrintHostType>(htOctoPrint));
@@ -4891,14 +5001,23 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(true));
def = this->add("wall_filament", coInt);
def = this->add("outer_wall_filament_id", coInt);
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
def->label = L("Walls");
def->label = L("Outer walls");
def->category = L("Extruders");
def->tooltip = L("Filament to print walls.");
def->min = 1;
def->tooltip = L("Filament to print outer walls.\n\"Default\" uses the active object/part filament.");
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(1));
def->set_default_value(new ConfigOptionInt(0));
def = this->add("inner_wall_filament_id", coInt);
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
def->label = L("Inner walls");
def->category = L("Extruders");
def->tooltip = L("Filament to print inner walls.\n\"Default\" uses the active object/part filament.");
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(0));
def = this->add("inner_wall_line_width", coFloatOrPercent);
def->label = L("Inner wall");
@@ -5652,14 +5771,32 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(15));
def = this->add("solid_infill_filament", coInt);
def = this->add("internal_solid_filament_id", coInt);
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
def->label = L("Solid infill");
def->label = L("Internal solid infill");
def->category = L("Extruders");
def->tooltip = L("Filament to print solid infill.");
def->min = 1;
def->tooltip = L("Filament to print internal solid infill.\n\"Default\" uses the active object/part filament.");
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(1));
def->set_default_value(new ConfigOptionInt(0));
def = this->add("top_surface_filament_id", coInt);
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
def->label = L("Top surface");
def->category = L("Extruders");
def->tooltip = L("Filament to print top surface.\n\"Default\" uses the active object/part filament.");
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(0));
def = this->add("bottom_surface_filament_id", coInt);
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
def->label = L("Bottom surface");
def->category = L("Extruders");
def->tooltip = L("Filament to print bottom surface.\n\"Default\" uses the active object/part filament.");
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(0));
def = this->add("internal_solid_infill_line_width", coFloatOrPercent);
def->label = L("Internal solid infill");
@@ -6035,7 +6172,7 @@ void PrintConfigDef::init_fff_params()
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
def->label = L("Support/raft base");
def->category = L("Support");
def->tooltip = L("Filament to print support base and raft. \"Default\" means no specific filament for support and current filament is used.");
def->tooltip = L("Filament to print support base and raft.\n\"Default\" means no specific filament for support and current filament is used.");
def->min = 0;
def->mode = comSimple;
def->set_default_value(new ConfigOptionInt(0));
@@ -6070,7 +6207,7 @@ void PrintConfigDef::init_fff_params()
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
def->label = L("Support/raft interface");
def->category = L("Support");
def->tooltip = L("Filament to print support interface. \"Default\" means no specific filament for support interface and current filament is used.");
def->tooltip = L("Filament to print support interface.\n\"Default\" means no specific filament for support interface and current filament is used.");
def->min = 0;
// BBS
def->mode = comSimple;
@@ -7888,12 +8025,34 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
opt_key = "change_filament_gcode";
} else if (opt_key == "bridge_fan_speed") {
opt_key = "overhang_fan_speed";
} else if (opt_key == "infill_extruder") {
opt_key = "sparse_infill_filament";
}else if (opt_key == "solid_infill_extruder") {
opt_key = "solid_infill_filament";
}else if (opt_key == "perimeter_extruder") {
opt_key = "wall_filament";
} else if (opt_key == "infill_extruder" || opt_key == "sparse_infill_filament") {
// ORCA: legacy feature-filament selector. Pre-2.4.0-dev these keys were 1-based and the
// default value "1" meant "the first/active filament". The current scheme uses a dedicated
// key where 0 = "Default" (inherit the object/part filament) and 1..N = explicit filament.
// Renaming to the new *_id key here means every config (process presets, 3mf project
// settings, imported gcode) is translated uniformly on load - not just the version-gated
// 3mf paths the old bespoke migration covered - and a brand-new key can never be misread as
// a legacy default. Map the legacy default "1" to "0" (inherit); keep explicit values >1.
opt_key = "sparse_infill_filament_id";
if (value == "1") value = "0";
} else if (opt_key == "solid_infill_extruder" || opt_key == "solid_infill_filament") {
opt_key = "internal_solid_filament_id";
if (value == "1") value = "0";
} else if (opt_key == "top_solid_infill_filament") {
opt_key = "top_surface_filament_id";
if (value == "1") value = "0";
} else if (opt_key == "bottom_solid_infill_filament") {
opt_key = "bottom_surface_filament_id";
if (value == "1") value = "0";
} else if (opt_key == "perimeter_extruder" || opt_key == "wall_filament" || opt_key == "wall_filament_id") {
opt_key = "outer_wall_filament_id";
if (value == "1") value = "0";
} else if (opt_key == "inner_wall_filament") {
opt_key = "inner_wall_filament_id";
if (value == "1") value = "0";
} else if (opt_key == "outer_wall_filament") {
opt_key = "outer_wall_filament_id";
if (value == "1") value = "0";
}else if(opt_key == "wipe_tower_extruder") {
opt_key = "wipe_tower_filament";
}else if (opt_key == "support_material_extruder") {
@@ -8342,10 +8501,18 @@ void DynamicPrintConfig::normalize_fdm(int used_filaments)
int extruder = this->option("extruder")->getInt();
this->erase("extruder");
if (extruder != 0) {
if (!this->has("sparse_infill_filament"))
this->option("sparse_infill_filament", true)->setInt(extruder);
if (!this->has("wall_filament"))
this->option("wall_filament", true)->setInt(extruder);
if (!this->has("sparse_infill_filament_id") || this->option("sparse_infill_filament_id")->getInt() == 0)
this->option("sparse_infill_filament_id", true)->setInt(extruder);
if (!this->has("outer_wall_filament_id") || this->option("outer_wall_filament_id")->getInt() == 0)
this->option("outer_wall_filament_id", true)->setInt(extruder);
if (!this->has("inner_wall_filament_id") || this->option("inner_wall_filament_id")->getInt() == 0)
this->option("inner_wall_filament_id", true)->setInt(extruder);
if (!this->has("internal_solid_filament_id") || this->option("internal_solid_filament_id")->getInt() == 0)
this->option("internal_solid_filament_id", true)->setInt(extruder);
if (!this->has("top_surface_filament_id") || this->option("top_surface_filament_id")->getInt() == 0)
this->option("top_surface_filament_id", true)->setInt(extruder);
if (!this->has("bottom_surface_filament_id") || this->option("bottom_surface_filament_id")->getInt() == 0)
this->option("bottom_surface_filament_id", true)->setInt(extruder);
// Don't propagate the current extruder to support.
// For non-soluble supports, the default "0" extruder means to use the active extruder,
// for soluble supports one certainly does not want to set the extruder to non-soluble.
@@ -8356,8 +8523,24 @@ void DynamicPrintConfig::normalize_fdm(int used_filaments)
}
}
if (!this->has("solid_infill_filament") && this->has("sparse_infill_filament"))
this->option("solid_infill_filament", true)->setInt(this->option("sparse_infill_filament")->getInt());
if (this->has("sparse_infill_filament_id")) {
int sparse_infill_filament_id = this->option("sparse_infill_filament_id")->getInt();
if (sparse_infill_filament_id > 0 && (!this->has("internal_solid_filament_id") || this->option("internal_solid_filament_id")->getInt() == 0))
this->option("internal_solid_filament_id", true)->setInt(sparse_infill_filament_id);
}
const int internal_solid = this->has("internal_solid_filament_id") ? this->option("internal_solid_filament_id")->getInt() : 0;
const int top_surface = this->has("top_surface_filament_id") ? this->option("top_surface_filament_id")->getInt() : 0;
const int bottom_surface = this->has("bottom_surface_filament_id") ? this->option("bottom_surface_filament_id")->getInt() : 0;
if (internal_solid == 0 && top_surface > 0)
this->option("internal_solid_filament_id", true)->setInt(top_surface);
if (internal_solid == 0 && bottom_surface > 0)
this->option("internal_solid_filament_id", true)->setInt(bottom_surface);
if (top_surface == 0 && internal_solid > 0)
this->option("top_surface_filament_id", true)->setInt(internal_solid);
if (bottom_surface == 0 && internal_solid > 0)
this->option("bottom_surface_filament_id", true)->setInt(internal_solid);
if (this->has("spiral_mode") && this->opt<ConfigOptionBool>("spiral_mode", true)->value) {
{
@@ -8415,10 +8598,18 @@ void DynamicPrintConfig::normalize_fdm_1()
int extruder = this->option("extruder")->getInt();
this->erase("extruder");
if (extruder != 0) {
if (!this->has("sparse_infill_filament"))
this->option("sparse_infill_filament", true)->setInt(extruder);
if (!this->has("wall_filament"))
this->option("wall_filament", true)->setInt(extruder);
if (!this->has("sparse_infill_filament_id") || this->option("sparse_infill_filament_id")->getInt() == 0)
this->option("sparse_infill_filament_id", true)->setInt(extruder);
if (!this->has("outer_wall_filament_id") || this->option("outer_wall_filament_id")->getInt() == 0)
this->option("outer_wall_filament_id", true)->setInt(extruder);
if (!this->has("inner_wall_filament_id") || this->option("inner_wall_filament_id")->getInt() == 0)
this->option("inner_wall_filament_id", true)->setInt(extruder);
if (!this->has("internal_solid_filament_id") || this->option("internal_solid_filament_id")->getInt() == 0)
this->option("internal_solid_filament_id", true)->setInt(extruder);
if (!this->has("top_surface_filament_id") || this->option("top_surface_filament_id")->getInt() == 0)
this->option("top_surface_filament_id", true)->setInt(extruder);
if (!this->has("bottom_surface_filament_id") || this->option("bottom_surface_filament_id")->getInt() == 0)
this->option("bottom_surface_filament_id", true)->setInt(extruder);
// Don't propagate the current extruder to support.
// For non-soluble supports, the default "0" extruder means to use the active extruder,
// for soluble supports one certainly does not want to set the extruder to non-soluble.
@@ -8429,8 +8620,24 @@ void DynamicPrintConfig::normalize_fdm_1()
}
}
if (!this->has("solid_infill_filament") && this->has("sparse_infill_filament"))
this->option("solid_infill_filament", true)->setInt(this->option("sparse_infill_filament")->getInt());
if (this->has("sparse_infill_filament_id")) {
int sparse_infill_filament_id = this->option("sparse_infill_filament_id")->getInt();
if (sparse_infill_filament_id > 0 && (!this->has("internal_solid_filament_id") || this->option("internal_solid_filament_id")->getInt() == 0))
this->option("internal_solid_filament_id", true)->setInt(sparse_infill_filament_id);
}
const int internal_solid = this->has("internal_solid_filament_id") ? this->option("internal_solid_filament_id")->getInt() : 0;
const int top_surface = this->has("top_surface_filament_id") ? this->option("top_surface_filament_id")->getInt() : 0;
const int bottom_surface = this->has("bottom_surface_filament_id") ? this->option("bottom_surface_filament_id")->getInt() : 0;
if (internal_solid == 0 && top_surface > 0)
this->option("internal_solid_filament_id", true)->setInt(top_surface);
if (internal_solid == 0 && bottom_surface > 0)
this->option("internal_solid_filament_id", true)->setInt(bottom_surface);
if (top_surface == 0 && internal_solid > 0)
this->option("top_surface_filament_id", true)->setInt(internal_solid);
if (bottom_surface == 0 && internal_solid > 0)
this->option("bottom_surface_filament_id", true)->setInt(internal_solid);
if (this->has("spiral_mode") && this->opt<ConfigOptionBool>("spiral_mode", true)->value) {
{
@@ -9637,6 +9844,7 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: can not find opt define for %2%")%__LINE__%key;
continue;
}
switch (optdef->type) {
case coStrings:
{
@@ -10186,8 +10394,8 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
error_message.emplace("bridge_flow", L("invalid value ") + std::to_string(cfg.bridge_flow));
}
// --bridge-flow-ratio
if (cfg.bridge_flow <= 0) {
// --internal-bridge-flow-ratio
if (cfg.internal_bridge_flow <= 0) {
error_message.emplace("internal_bridge_flow", L("invalid value ") + std::to_string(cfg.internal_bridge_flow));
}
@@ -10245,13 +10453,18 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
// extrusion widths
{
double max_nozzle_diameter = 0.;
double min_nozzle_diameter = std::numeric_limits<double>::max();
for (double dmr : cfg.nozzle_diameter.values)
{
max_nozzle_diameter = std::max(max_nozzle_diameter, dmr);
min_nozzle_diameter = std::min(min_nozzle_diameter, dmr);
}
const char *widths[] = {
"outer_wall_line_width",
"inner_wall_line_width",
"sparse_infill_line_width",
"internal_solid_infill_line_width",
"bridge_line_width",
"top_surface_line_width",
"support_line_width",
"initial_layer_line_width",
@@ -10259,8 +10472,13 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
"skeleton_infill_line_width"};
for (size_t i = 0; i < sizeof(widths) / sizeof(widths[i]); ++ i) {
std::string key(widths[i]);
if (cfg.get_abs_value(key, max_nozzle_diameter) > MAX_LINE_WIDTH_MULTIPLIER * max_nozzle_diameter) {
error_message.emplace(key, L("too large line width ") + std::to_string(cfg.get_abs_value(key)));
double abs_width = cfg.get_abs_value(key, max_nozzle_diameter);
double allowed_max = (key == "bridge_line_width") ? min_nozzle_diameter : MAX_LINE_WIDTH_MULTIPLIER * max_nozzle_diameter;
if (abs_width > allowed_max) {
if (key == "bridge_line_width")
error_message.emplace(key, L("Bridge line width must not exceed nozzle diameter: ") + std::to_string(abs_width));
else
error_message.emplace(key, L("too large line width ") + std::to_string(abs_width));
//return std::string("Too Large line width: ") + key;
}
}
@@ -10723,6 +10941,12 @@ CLIMiscConfigDef::CLIMiscConfigDef()
def->cli_params = "level";
def->set_default_value(new ConfigOptionInt(1));
def = this->add("logfile", coInt);
def->label = L("Log file");
def->tooltip = L("Redirects debug logging to file.\n");
def->cli_params = "file";
def->set_default_value(new ConfigOptionString());
def = this->add("enable_timelapse", coBool);
def->label = L("Enable timelapse for print");
def->tooltip = L("If enabled, this slicing will be considered using timelapse.");
+15 -4
View File
@@ -77,7 +77,7 @@ enum class WipeTowerType {
};
enum PrintHostType {
htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink
htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink, ht3DPrinterOS, htMoonraker
};
enum AuthorizationType {
@@ -1080,7 +1080,9 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloat, bottom_shell_thickness))
((ConfigOptionFloat, bridge_angle))
((ConfigOptionFloat, internal_bridge_angle)) // ORCA: Internal bridge angle override
((ConfigOptionBool, relative_bridge_angle)) // ORCA: Relative bridge angle flag
((ConfigOptionFloat, bridge_flow))
((ConfigOptionFloatOrPercent, bridge_line_width))
((ConfigOptionFloat, internal_bridge_flow))
((ConfigOptionFloat, bridge_speed))
((ConfigOptionFloatOrPercent, internal_bridge_speed))
@@ -1103,6 +1105,9 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloat, lateral_lattice_angle_1))
((ConfigOptionFloat, lateral_lattice_angle_2))
((ConfigOptionFloat, infill_overhang_angle))
((ConfigOptionFloat, lightning_overhang_angle))
((ConfigOptionFloat, lightning_prune_angle))
((ConfigOptionFloat, lightning_straightening_angle))
((ConfigOptionBool, align_infill_direction_to_model))
((ConfigOptionString, extra_solid_infills))
((ConfigOptionEnum<FuzzySkinType>, fuzzy_skin))
@@ -1118,7 +1123,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionPercent, fuzzy_skin_ripple_offset))
((ConfigOptionInt, fuzzy_skin_layers_between_ripple_offset))
((ConfigOptionFloat, gap_infill_speed))
((ConfigOptionInt, sparse_infill_filament))
((ConfigOptionInt, sparse_infill_filament_id))
((ConfigOptionFloatOrPercent, sparse_infill_line_width))
((ConfigOptionPercent, infill_wall_overlap))
((ConfigOptionPercent, top_bottom_infill_wall_overlap))
@@ -1151,14 +1156,17 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloatsNullable, filament_ironing_speed))
// Detect bridging perimeters
((ConfigOptionBool, detect_overhang_wall))
((ConfigOptionInt, wall_filament))
((ConfigOptionInt, outer_wall_filament_id))
((ConfigOptionInt, inner_wall_filament_id))
((ConfigOptionFloatOrPercent, inner_wall_line_width))
((ConfigOptionFloat, inner_wall_speed))
// Total number of perimeters.
((ConfigOptionInt, wall_loops))
((ConfigOptionBool, alternate_extra_wall))
((ConfigOptionFloat, minimum_sparse_infill_area))
((ConfigOptionInt, solid_infill_filament))
((ConfigOptionInt, internal_solid_filament_id))
((ConfigOptionInt, top_surface_filament_id))
((ConfigOptionInt, bottom_surface_filament_id))
((ConfigOptionFloatOrPercent, internal_solid_infill_line_width))
((ConfigOptionFloat, internal_solid_infill_speed))
// Detect thin walls.
@@ -1480,6 +1488,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionFloatOrPercent, max_travel_detour_distance))
((ConfigOptionPoints, printable_area))
((ConfigOptionPointsGroups, extruder_printable_area))
((ConfigOptionBool, support_parallel_printheads))
((ConfigOptionInt, parallel_printheads_count))
((ConfigOptionStrings, parallel_printheads_bed_exclude_areas))
//BBS: add bed_exclude_area
((ConfigOptionPoints, bed_exclude_area))
((ConfigOptionPoints, head_wrap_detect_zone))
+181 -63
View File
@@ -232,7 +232,7 @@ void PrintObject::_transform_hole_to_polyholes()
bool twist = this->m_layers[layer_idx]->m_regions[region_idx]->region().config().hole_to_polyhole_twisted.value;
if (diameter_max - diameter_min < max_variation * 2 && diameter_line_max - diameter_line_min < max_variation * 2) {
layerid2center[layer_idx].emplace_back(
std::tuple<Point, float, int, coord_t, bool>{center, diameter_max, layer->m_regions[region_idx]->region().config().wall_filament.value, max_variation, twist}, & hole);
std::tuple<Point, float, int, coord_t, bool>{center, diameter_max, layer->m_regions[region_idx]->region().config().outer_wall_filament_id.value, max_variation, twist}, & hole);
}
}
}
@@ -316,18 +316,29 @@ std::vector<std::set<int>> PrintObject::detect_extruder_geometric_unprintables()
continue;
for (auto layerm : layer->regions()) {
auto region = layerm->region();
int wall_filament = region.config().wall_filament;
int solid_infill_filament = region.config().solid_infill_filament;
int sparse_infill_filament = region.config().sparse_infill_filament;
int outer_wall_filament_id = region.config().outer_wall_filament_id;
int inner_wall_filament_id = region.config().inner_wall_filament_id;
int internal_solid_filament_id = region.config().internal_solid_filament_id;
int top_surface_filament_id = region.config().top_surface_filament_id;
int bottom_surface_filament_id = region.config().bottom_surface_filament_id;
int sparse_infill_filament_id = region.config().sparse_infill_filament_id;
if (!layerm->fills.entities.empty()) {
if (solid_infill_filament > 0)
geometric_unprintables[extruder_id].insert(solid_infill_filament - 1);
if (sparse_infill_filament > 0)
geometric_unprintables[extruder_id].insert(sparse_infill_filament - 1);
if (internal_solid_filament_id > 0)
geometric_unprintables[extruder_id].insert(internal_solid_filament_id - 1);
if (top_surface_filament_id > 0)
geometric_unprintables[extruder_id].insert(top_surface_filament_id - 1);
if (bottom_surface_filament_id > 0)
geometric_unprintables[extruder_id].insert(bottom_surface_filament_id - 1);
if (sparse_infill_filament_id > 0)
geometric_unprintables[extruder_id].insert(sparse_infill_filament_id - 1);
}
if (!layerm->perimeters.entities.empty()) {
if (outer_wall_filament_id > 0)
geometric_unprintables[extruder_id].insert(outer_wall_filament_id - 1);
if (inner_wall_filament_id > 0)
geometric_unprintables[extruder_id].insert(inner_wall_filament_id - 1);
}
if (!layerm->perimeters.entities.empty() && wall_filament > 0)
geometric_unprintables[extruder_id].insert(wall_filament - 1);
}
}
}
@@ -352,21 +363,28 @@ std::vector<std::set<int>> PrintObject::detect_extruder_geometric_unprintables()
auto layer = m_layers[j];
for (auto layerm : layer->regions()) {
const auto& region = layerm->region();
int wall_filament = region.config().wall_filament;
int solid_infill_filament = region.config().solid_infill_filament;
int sparse_infill_filament = region.config().sparse_infill_filament;
int outer_wall_filament_id = region.config().outer_wall_filament_id;
int inner_wall_filament_id = region.config().inner_wall_filament_id;
int internal_solid_filament_id = region.config().internal_solid_filament_id;
int top_surface_filament_id = region.config().top_surface_filament_id;
int bottom_surface_filament_id = region.config().bottom_surface_filament_id;
int sparse_infill_filament_id = region.config().sparse_infill_filament_id;
std::optional<ExPolygons> fill_expolys;
BoundingBox fill_bbox;
std::optional<ExPolygons> wall_expolys;
BoundingBox wall_bbox;
for (size_t idx = 0; idx < unprintable_area_in_obj_coord.size(); ++idx) {
bool do_infill_filament_detect = (solid_infill_filament > 0 && tbb_geometric_unprintables[idx].count(solid_infill_filament - 1) == 0) ||
(sparse_infill_filament > 0 && tbb_geometric_unprintables[idx].count(sparse_infill_filament-1) == 0);
bool do_infill_filament_detect = (internal_solid_filament_id > 0 && tbb_geometric_unprintables[idx].count(internal_solid_filament_id - 1) == 0) ||
(top_surface_filament_id > 0 && tbb_geometric_unprintables[idx].count(top_surface_filament_id - 1) == 0) ||
(bottom_surface_filament_id > 0 && tbb_geometric_unprintables[idx].count(bottom_surface_filament_id - 1) == 0) ||
(sparse_infill_filament_id > 0 && tbb_geometric_unprintables[idx].count(sparse_infill_filament_id-1) == 0);
bool infill_unprintable = !layerm->fills.entities.empty() &&
((solid_infill_filament > 0 && tbb_geometric_unprintables[idx].count(solid_infill_filament - 1) > 0) ||
(sparse_infill_filament > 0 && tbb_geometric_unprintables[idx].count(sparse_infill_filament - 1) > 0));
((internal_solid_filament_id > 0 && tbb_geometric_unprintables[idx].count(internal_solid_filament_id - 1) > 0) ||
(top_surface_filament_id > 0 && tbb_geometric_unprintables[idx].count(top_surface_filament_id - 1) > 0) ||
(bottom_surface_filament_id > 0 && tbb_geometric_unprintables[idx].count(bottom_surface_filament_id - 1) > 0) ||
(sparse_infill_filament_id > 0 && tbb_geometric_unprintables[idx].count(sparse_infill_filament_id - 1) > 0));
if (!layerm->fills.entities.empty() && do_infill_filament_detect) {
if (!fill_expolys) {
@@ -375,19 +393,27 @@ std::vector<std::set<int>> PrintObject::detect_extruder_geometric_unprintables()
}
if (fill_bbox.overlap(unprintable_area_bbox[idx]) &&
!intersection(*fill_expolys, unprintable_area_in_obj_coord[idx]).empty()) {
if (solid_infill_filament > 0)
tbb_geometric_unprintables[idx].insert(solid_infill_filament - 1);
if (sparse_infill_filament > 0)
tbb_geometric_unprintables[idx].insert(sparse_infill_filament - 1);
if (internal_solid_filament_id > 0)
tbb_geometric_unprintables[idx].insert(internal_solid_filament_id - 1);
if (top_surface_filament_id > 0)
tbb_geometric_unprintables[idx].insert(top_surface_filament_id - 1);
if (bottom_surface_filament_id > 0)
tbb_geometric_unprintables[idx].insert(bottom_surface_filament_id - 1);
if (sparse_infill_filament_id > 0)
tbb_geometric_unprintables[idx].insert(sparse_infill_filament_id - 1);
infill_unprintable = true;
}
}
bool do_wall_filament_detect = wall_filament > 0 && tbb_geometric_unprintables[idx].count(wall_filament - 1) == 0;
if (!layerm->perimeters.entities.empty() && do_wall_filament_detect) {
bool do_outer_wall_filament_detect = outer_wall_filament_id > 0 && tbb_geometric_unprintables[idx].count(outer_wall_filament_id - 1) == 0;
bool do_inner_wall_filament_detect = inner_wall_filament_id > 0 && tbb_geometric_unprintables[idx].count(inner_wall_filament_id - 1) == 0;
if (!layerm->perimeters.entities.empty() && (do_outer_wall_filament_detect || do_inner_wall_filament_detect)) {
// if infill is unprintable, no need to check wall since wall contour surrounds infill contour
if (infill_unprintable) {
tbb_geometric_unprintables[idx].insert(wall_filament - 1);
if (outer_wall_filament_id > 0)
tbb_geometric_unprintables[idx].insert(outer_wall_filament_id - 1);
if (inner_wall_filament_id > 0)
tbb_geometric_unprintables[idx].insert(inner_wall_filament_id - 1);
continue;
}
@@ -402,7 +428,10 @@ std::vector<std::set<int>> PrintObject::detect_extruder_geometric_unprintables()
if (wall_bbox.overlap(unprintable_area_bbox[idx]) &&
!intersection(*wall_expolys, unprintable_area_in_obj_coord[idx]).empty()) {
tbb_geometric_unprintables[idx].insert(wall_filament - 1);
if (outer_wall_filament_id > 0)
tbb_geometric_unprintables[idx].insert(outer_wall_filament_id - 1);
if (inner_wall_filament_id > 0)
tbb_geometric_unprintables[idx].insert(inner_wall_filament_id - 1);
}
}
}
@@ -1263,8 +1292,10 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "bottom_shell_thickness"
|| opt_key == "top_shell_thickness"
|| opt_key == "minimum_sparse_infill_area"
|| opt_key == "sparse_infill_filament"
|| opt_key == "solid_infill_filament"
|| opt_key == "sparse_infill_filament_id"
|| opt_key == "internal_solid_filament_id"
|| opt_key == "top_surface_filament_id"
|| opt_key == "bottom_surface_filament_id"
|| opt_key == "sparse_infill_line_width"
|| opt_key == "skin_infill_line_width"
|| opt_key == "skeleton_infill_line_width"
@@ -1275,7 +1306,9 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "ensure_vertical_shell_thickness"
|| opt_key == "bridge_angle"
|| opt_key == "internal_bridge_angle" // ORCA: Internal bridge angle override
|| opt_key == "relative_bridge_angle" // ORCA: Relative bridge angle
//BBS
|| opt_key == "bridge_line_width"
|| opt_key == "bridge_density"
|| opt_key == "internal_bridge_density") {
steps.emplace_back(posPrepareInfill);
@@ -1300,6 +1333,9 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "infill_shift_step"
|| opt_key == "sparse_infill_rotate_template"
|| opt_key == "solid_infill_rotate_template"
|| opt_key == "lightning_overhang_angle"
|| opt_key == "lightning_prune_angle"
|| opt_key == "lightning_straightening_angle"
|| opt_key == "skeleton_infill_density"
|| opt_key == "skin_infill_density"
|| opt_key == "infill_lock_depth"
@@ -1322,7 +1358,8 @@ bool PrintObject::invalidate_state_by_config_options(
steps.emplace_back(posPrepareInfill);
} else if (
opt_key == "outer_wall_line_width"
|| opt_key == "wall_filament"
|| opt_key == "outer_wall_filament_id"
|| opt_key == "inner_wall_filament_id"
|| opt_key == "fuzzy_skin"
|| opt_key == "fuzzy_skin_thickness"
|| opt_key == "fuzzy_skin_point_distance"
@@ -3214,8 +3251,19 @@ void PrintObject::bridge_over_infill()
}
// ORCA: Internal bridge angle override
if (candidate.region->region().config().internal_bridge_angle > 0)
bridging_angle = candidate.region->region().config().internal_bridge_angle.value * PI / 180.0; // Convert degrees to radians
if (candidate.region->region().config().internal_bridge_angle.value > 0) {
const auto &region_config = candidate.region->region().config();
const double custom_angle_rad = Geometry::deg2rad(region_config.internal_bridge_angle.value);
if (region_config.relative_bridge_angle.value)
bridging_angle += custom_angle_rad;
else {
bridging_angle = custom_angle_rad;
if (region_config.align_infill_direction_to_model) {
auto m = po->trafo().matrix();
bridging_angle += std::atan2((double)m(1, 0), (double)m(0, 0));
}
}
}
boundary_plines.insert(boundary_plines.end(), anchors.begin(), anchors.end());
if (!lightning_area.empty() && !intersection(area_to_be_bridge, lightning_area).empty()) {
@@ -3511,6 +3559,12 @@ static void clamp_exturder_to_default(ConfigOptionInt &opt, size_t num_extruders
opt.value = 1;
}
static void clamp_feature_filament_to_valid(ConfigOptionInt &opt, size_t num_extruders)
{
if (opt.value <= 0 || opt.value > (int)num_extruders)
opt.value = 1;
}
PrintObjectConfig PrintObject::object_config_from_model_object(const PrintObjectConfig &default_object_config, const ModelObject &object, size_t num_extruders)
{
PrintObjectConfig config = default_object_config;
@@ -3526,63 +3580,124 @@ PrintObjectConfig PrintObject::object_config_from_model_object(const PrintObject
}
const std::string key_extruder { "extruder" };
static constexpr const std::initializer_list<const std::string_view> keys_extruders { "sparse_infill_filament"sv, "solid_infill_filament"sv, "wall_filament"sv };
static constexpr const std::initializer_list<const std::string_view> keys_extruders {
"sparse_infill_filament_id"sv,
"internal_solid_filament_id"sv,
"top_surface_filament_id"sv,
"bottom_surface_filament_id"sv,
"outer_wall_filament_id"sv,
"inner_wall_filament_id"sv
};
static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPrintConfig &in)
struct FeatureFilamentOverrideMask
{
// 1) Map legacy "extruder" to feature filament keys as a fallback only.
// If any feature-specific filament is explicitly set, keep those values.
bool sparse_infill_filament_id = false;
bool internal_solid_filament_id = false;
bool top_surface_filament_id = false;
bool bottom_surface_filament_id = false;
bool outer_wall_filament_id = false;
bool inner_wall_filament_id = false;
};
static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPrintConfig &in, FeatureFilamentOverrideMask &feature_overrides)
{
// 1) Explicit feature filament values take precedence over base extruder fallback.
auto *opt_extruder = in.opt<ConfigOptionInt>(key_extruder);
auto *opt_sparse_infill_filament = in.opt<ConfigOptionInt>("sparse_infill_filament");
auto *opt_solid_infill_filament = in.opt<ConfigOptionInt>("solid_infill_filament");
auto *opt_wall_filament = in.opt<ConfigOptionInt>("wall_filament");
const bool has_feature_filament_override =
(opt_sparse_infill_filament != nullptr && opt_sparse_infill_filament->value > 0) ||
(opt_solid_infill_filament != nullptr && opt_solid_infill_filament->value > 0) ||
(opt_wall_filament != nullptr && opt_wall_filament->value > 0);
if (opt_extruder)
if (int extruder = opt_extruder->value; extruder > 1 && ! has_feature_filament_override) {
// Not a default extruder.
out.sparse_infill_filament.value = extruder;
out.solid_infill_filament.value = extruder;
out.wall_filament.value = extruder;
}
int base_extruder = (opt_extruder != nullptr) ? opt_extruder->value : 0;
// 2) Copy the rest of the values.
for (auto it = in.cbegin(); it != in.cend(); ++ it)
if (it->first != key_extruder)
if (ConfigOption* my_opt = out.option(it->first, false); my_opt != nullptr) {
if (one_of(it->first, keys_extruders)) {
// Ignore "default" extruders.
// "Default" (0) clears explicit override for this scope and lets fallback apply.
int extruder = static_cast<const ConfigOptionInt*>(it->second.get())->value;
if (extruder > 0)
if (extruder > 0) {
my_opt->setInt(extruder);
if (it->first == "sparse_infill_filament_id")
feature_overrides.sparse_infill_filament_id = true;
else if (it->first == "internal_solid_filament_id")
feature_overrides.internal_solid_filament_id = true;
else if (it->first == "top_surface_filament_id")
feature_overrides.top_surface_filament_id = true;
else if (it->first == "bottom_surface_filament_id")
feature_overrides.bottom_surface_filament_id = true;
else if (it->first == "outer_wall_filament_id")
feature_overrides.outer_wall_filament_id = true;
else if (it->first == "inner_wall_filament_id")
feature_overrides.inner_wall_filament_id = true;
} else {
if (it->first == "sparse_infill_filament_id")
feature_overrides.sparse_infill_filament_id = false;
else if (it->first == "internal_solid_filament_id")
feature_overrides.internal_solid_filament_id = false;
else if (it->first == "top_surface_filament_id")
feature_overrides.top_surface_filament_id = false;
else if (it->first == "bottom_surface_filament_id")
feature_overrides.bottom_surface_filament_id = false;
else if (it->first == "outer_wall_filament_id")
feature_overrides.outer_wall_filament_id = false;
else if (it->first == "inner_wall_filament_id")
feature_overrides.inner_wall_filament_id = false;
}
} else
my_opt->set(it->second.get());
}
// 3) Apply base extruder only to features that were not explicitly overridden.
if (base_extruder > 0) {
if (!feature_overrides.sparse_infill_filament_id)
out.sparse_infill_filament_id.value = base_extruder;
if (!feature_overrides.internal_solid_filament_id)
out.internal_solid_filament_id.value = base_extruder;
if (!feature_overrides.top_surface_filament_id)
out.top_surface_filament_id.value = base_extruder;
if (!feature_overrides.bottom_surface_filament_id)
out.bottom_surface_filament_id.value = base_extruder;
if (!feature_overrides.outer_wall_filament_id)
out.outer_wall_filament_id.value = base_extruder;
if (!feature_overrides.inner_wall_filament_id)
out.inner_wall_filament_id.value = base_extruder;
}
}
PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &default_or_parent_region_config, const DynamicPrintConfig *layer_range_config, const ModelVolume &volume, size_t num_extruders)
{
PrintRegionConfig config = default_or_parent_region_config;
FeatureFilamentOverrideMask feature_overrides;
// For model parts, non-zero values coming from the print defaults should stay explicit.
if (volume.is_model_part()) {
feature_overrides.sparse_infill_filament_id = (config.sparse_infill_filament_id.value > 0);
feature_overrides.internal_solid_filament_id = (config.internal_solid_filament_id.value > 0);
feature_overrides.top_surface_filament_id = (config.top_surface_filament_id.value > 0);
feature_overrides.bottom_surface_filament_id = (config.bottom_surface_filament_id.value > 0);
feature_overrides.outer_wall_filament_id = (config.outer_wall_filament_id.value > 0);
feature_overrides.inner_wall_filament_id = (config.inner_wall_filament_id.value > 0);
}
if (volume.is_model_part()) {
// default_or_parent_region_config contains the Print's PrintRegionConfig.
// Override with ModelObject's PrintRegionConfig values.
apply_to_print_region_config(config, volume.get_object()->config.get());
apply_to_print_region_config(config, volume.get_object()->config.get(), feature_overrides);
} else {
// default_or_parent_region_config contains parent PrintRegion config, which already contains ModelVolume's config.
}
apply_to_print_region_config(config, volume.config.get());
apply_to_print_region_config(config, volume.config.get(), feature_overrides);
if (! volume.material_id().empty())
apply_to_print_region_config(config, volume.material()->config.get());
apply_to_print_region_config(config, volume.material()->config.get(), feature_overrides);
if (layer_range_config != nullptr) {
// Not applicable to modifiers.
assert(volume.is_model_part());
apply_to_print_region_config(config, *layer_range_config);
apply_to_print_region_config(config, *layer_range_config, feature_overrides);
}
// Clamp invalid extruders to the default extruder (with index 1).
clamp_exturder_to_default(config.sparse_infill_filament, num_extruders);
clamp_exturder_to_default(config.wall_filament, num_extruders);
clamp_exturder_to_default(config.solid_infill_filament, num_extruders);
// Resolve feature defaults and clamp invalid extruders to index 1.
clamp_feature_filament_to_valid(config.sparse_infill_filament_id, num_extruders);
clamp_feature_filament_to_valid(config.outer_wall_filament_id, num_extruders);
clamp_feature_filament_to_valid(config.inner_wall_filament_id, num_extruders);
clamp_feature_filament_to_valid(config.internal_solid_filament_id, num_extruders);
clamp_feature_filament_to_valid(config.top_surface_filament_id, num_extruders);
clamp_feature_filament_to_valid(config.bottom_surface_filament_id, num_extruders);
if (config.sparse_infill_density.value < 0.00011f)
// Switch of infill for very low infill rates, also avoid division by zero in infill generator for these very low rates.
// See GH issue #5910.
@@ -3645,9 +3760,12 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full
object_config.brim_type != btNoBrim && object_config.brim_width > 0.,
object_extruders);
for (const std::pair<const t_layer_height_range, ModelConfig> &range_and_config : model_object.layer_config_ranges)
if (range_and_config.second.has("wall_filament") ||
range_and_config.second.has("sparse_infill_filament") ||
range_and_config.second.has("solid_infill_filament"))
if (range_and_config.second.has("outer_wall_filament_id") ||
range_and_config.second.has("inner_wall_filament_id") ||
range_and_config.second.has("sparse_infill_filament_id") ||
range_and_config.second.has("internal_solid_filament_id") ||
range_and_config.second.has("top_surface_filament_id") ||
range_and_config.second.has("bottom_surface_filament_id"))
PrintRegion::collect_object_printing_extruders(
print_config,
region_config_from_model_volume(default_region_config, &range_and_config.second.get(), *model_volume, filament_extruders),
@@ -4064,8 +4182,8 @@ void PrintObject::combine_infill()
// Limit the number of combined layers to the maximum height allowed by this regions' nozzle.
//FIXME limit the layer height to max_layer_height
double nozzle_diameter = std::min(
this->print()->config().nozzle_diameter.get_at(region.config().sparse_infill_filament.value - 1),
this->print()->config().nozzle_diameter.get_at(region.config().solid_infill_filament.value - 1));
this->print()->config().nozzle_diameter.get_at(region.config().sparse_infill_filament_id.value - 1),
this->print()->config().nozzle_diameter.get_at(region.config().internal_solid_filament_id.value - 1));
//Orca: Limit combination of infill to up to infill_combination_max_layer_height
const double infill_combination_max_layer_height = region.config().infill_combination_max_layer_height.get_abs_value(nozzle_diameter);
+1 -1
View File
@@ -359,7 +359,7 @@ static std::vector<std::vector<ExPolygons>> slices_to_regions(
bool rhs_empty = rhs.region_id < 0 || rhs.expolygons.empty();
// Sort the empty items to the end of the list.
// Sort by region_id & volume_id lexicographically.
return ! this_empty && (rhs_empty || (this->region_id < rhs.region_id || (this->region_id == rhs.region_id && volume_id < volume_id)));
return ! this_empty && (rhs_empty || (this->region_id < rhs.region_id || (this->region_id == rhs.region_id && volume_id < rhs.volume_id)));
}
};
+33 -16
View File
@@ -7,12 +7,16 @@ namespace Slic3r {
unsigned int PrintRegion::extruder(FlowRole role) const
{
size_t extruder = 0;
if (role == frPerimeter || role == frExternalPerimeter)
extruder = m_config.wall_filament;
if (role == frPerimeter)
extruder = m_config.inner_wall_filament_id;
else if (role == frExternalPerimeter)
extruder = m_config.outer_wall_filament_id;
else if (role == frInfill)
extruder = m_config.sparse_infill_filament;
else if (role == frSolidInfill || role == frTopSolidInfill)
extruder = m_config.solid_infill_filament;
extruder = m_config.sparse_infill_filament_id;
else if (role == frSolidInfill)
extruder = m_config.internal_solid_filament_id;
else if (role == frTopSolidInfill)
extruder = m_config.top_surface_filament_id;
else
throw Slic3r::InvalidArgument("Unknown role");
return extruder;
@@ -51,9 +55,12 @@ Flow PrintRegion::flow(const PrintObject &object, FlowRole role, double layer_he
coordf_t PrintRegion::nozzle_dmr_avg(const PrintConfig &print_config) const
{
return (print_config.nozzle_diameter.get_at(m_config.wall_filament.value - 1) +
print_config.nozzle_diameter.get_at(m_config.sparse_infill_filament.value - 1) +
print_config.nozzle_diameter.get_at(m_config.solid_infill_filament.value - 1)) / 3.;
return (print_config.nozzle_diameter.get_at(m_config.outer_wall_filament_id.value - 1) +
print_config.nozzle_diameter.get_at(m_config.inner_wall_filament_id.value - 1) +
print_config.nozzle_diameter.get_at(m_config.sparse_infill_filament_id.value - 1) +
print_config.nozzle_diameter.get_at(m_config.internal_solid_filament_id.value - 1) +
print_config.nozzle_diameter.get_at(m_config.top_surface_filament_id.value - 1) +
print_config.nozzle_diameter.get_at(m_config.bottom_surface_filament_id.value - 1)) / 6.;
}
coordf_t PrintRegion::bridging_height_avg(const PrintConfig &print_config) const
@@ -70,12 +77,19 @@ void PrintRegion::collect_object_printing_extruders(const PrintConfig &print_con
int i = std::max(0, extruder_id - 1);
object_extruders.emplace_back((i >= num_extruders) ? 0 : i);
};
if (region_config.wall_loops.value > 0 || has_brim)
emplace_extruder(region_config.wall_filament);
if (region_config.wall_loops.value > 0 || has_brim) {
emplace_extruder(region_config.outer_wall_filament_id);
if (region_config.wall_loops.value > 1)
emplace_extruder(region_config.inner_wall_filament_id);
}
if (region_config.sparse_infill_density.value > 0)
emplace_extruder(region_config.sparse_infill_filament);
if (region_config.top_shell_layers.value > 0 || region_config.bottom_shell_layers.value > 0)
emplace_extruder(region_config.solid_infill_filament);
emplace_extruder(region_config.sparse_infill_filament_id);
if (region_config.sparse_infill_density.value > 0 || region_config.top_shell_layers.value > 0 || region_config.bottom_shell_layers.value > 0)
emplace_extruder(region_config.internal_solid_filament_id);
if (region_config.top_shell_layers.value > 0)
emplace_extruder(region_config.top_surface_filament_id);
if (region_config.bottom_shell_layers.value > 0)
emplace_extruder(region_config.bottom_surface_filament_id);
}
void PrintRegion::collect_object_printing_extruders(const Print &print, std::vector<unsigned int> &object_extruders) const
@@ -85,9 +99,12 @@ void PrintRegion::collect_object_printing_extruders(const Print &print, std::vec
#ifndef NDEBUG
// BBS
auto num_extruders = int(print.config().filament_diameter.size());
assert(this->config().wall_filament <= num_extruders);
assert(this->config().sparse_infill_filament <= num_extruders);
assert(this->config().solid_infill_filament <= num_extruders);
assert(this->config().outer_wall_filament_id <= num_extruders);
assert(this->config().inner_wall_filament_id <= num_extruders);
assert(this->config().sparse_infill_filament_id <= num_extruders);
assert(this->config().internal_solid_filament_id <= num_extruders);
assert(this->config().top_surface_filament_id <= num_extruders);
assert(this->config().bottom_surface_filament_id <= num_extruders);
#endif
collect_object_printing_extruders(print.config(), this->config(), print.has_brim(), object_extruders);
}
+1 -1
View File
@@ -1226,7 +1226,7 @@ namespace SupportMaterialInternal {
// Surface supporting this layer, expanded by 0.5 * nozzle_diameter, as we consider this kind of overhang to be sufficiently supported.
Polygons lower_grown_slices = expand(lower_layer_polygons,
//FIXME to mimic the decision in the perimeter generator, we should use half the external perimeter width.
0.5f * float(scale_(print_config.nozzle_diameter.get_at(layerm.region().config().wall_filament-1))),
0.5f * float(scale_(print_config.nozzle_diameter.get_at(layerm.region().config().outer_wall_filament_id-1))),
SUPPORT_SURFACES_OFFSET_PARAMETERS);
// Collect perimeters of this layer.
//FIXME split_at_first_point() could split a bridge mid-way
+56 -79
View File
@@ -1610,80 +1610,71 @@ void TreeSupport::generate_toolpaths()
filler_support->angle = Geometry::deg2rad(object_config.support_angle.value);
Polygons loops = to_polygons(poly);
//ORCA: Group base per area as no_sort to keep outline->fill together.
std::unique_ptr<ExtrusionEntityCollection> base_eec = std::make_unique<ExtrusionEntityCollection>();
base_eec->no_sort = true;
ExtrusionEntitiesPtr &base_dst = base_eec->entities;
if (layer_id == 0) {
float density = float(m_object_config->raft_first_layer_density.value * 0.01);
fill_expolygons_with_sheath_generate_paths(ts_layer->support_fills.entities, loops, filler_support.get(), density, erSupportMaterial, flow,
fill_expolygons_with_sheath_generate_paths(base_dst, loops, filler_support.get(), density, erSupportMaterial, flow,
m_support_params, true, false);
}
else {
//ORCA: Force base walls before infill to keep outline->fill order.
if (need_infill && m_support_params.base_fill_pattern != ipLightning) {
// allow infill-only mode if support is thick enough (so min_wall_count is 0);
// otherwise must draw 1 wall
// Don't need extra walls if we have infill. Extra walls may overlap with the infills.
size_t min_wall_count = offset(poly, -scale_(support_spacing * 1.5)).empty() ? 1 : 0;
make_perimeter_and_infill(ts_layer->support_fills.entities, poly, std::max(min_wall_count, wall_count), flow,
erSupportMaterial, filler_support.get(), support_density);
make_perimeter_and_infill(base_dst, poly, std::max(min_wall_count, wall_count), flow,
erSupportMaterial, filler_support.get(), support_density, false);
}
else {
SupportParameters support_params = m_support_params;
if (area_group.need_extra_wall && object_config.tree_support_wall_count.value == 0)
support_params.tree_branch_diameter_double_wall_area_scaled = 0.1;
tree_supports_generate_paths(ts_layer->support_fills.entities, loops, flow, support_params);
tree_supports_generate_paths(base_dst, loops, flow, support_params);
}
}
}
}
if (m_support_params.base_fill_pattern == ipLightning)
{
double print_z = ts_layer->print_z;
if (printZ_to_lightninglayer.find(print_z) == printZ_to_lightninglayer.end())
continue;
//TODO:
//1.the second parameter of convertToLines seems to decide how long the lightning should be trimmed from its root, so that the root wont overlap/detach the support contour.
// whether current value works correctly remained to be tested
//2.related to previous one, that lightning roots need to be trimed more when support has multiple walls
//3.function connect_infill() and variable 'params' helps create connection pattern along contours between two lightning roots,
// strengthen lightnings while it may make support harder. decide to enable it or not. if yes, proper values for params are remained to be tested
auto& lightning_layer = generator->getTreesForLayer(printZ_to_lightninglayer[print_z]);
Flow flow = (layer_id == 0 && m_raft_layers == 0) ? m_support_params.first_layer_flow : support_flow;
ExPolygons areas = offset_ex(ts_layer->base_areas, -flow.scaled_spacing());
for (auto& area : areas)
{
Polylines polylines = lightning_layer.convertToLines(to_polygons(area), 0);
for (auto itr = polylines.begin(); itr != polylines.end();)
{
if (itr->length() < scale_(1.0))
itr = polylines.erase(itr);
else
itr++;
}
Polylines opt_polylines;
#if 1
//this wont create connection patterns along contours
append(opt_polylines, chain_polylines(std::move(polylines)));
#else
//this will create connection patterns along contours
FillParams params;
params.anchor_length = float(Fill::infill_anchor * 0.01 * flow.spacing());
params.anchor_length_max = Fill::infill_anchor_max;
params.anchor_length = std::min(params.anchor_length, params.anchor_length_max);
Fill::connect_infill(std::move(polylines), area, opt_polylines, flow.spacing(), params);
#endif
extrusion_entities_append_paths(ts_layer->support_fills.entities, opt_polylines, erSupportMaterial,
float(flow.mm3_per_mm()), float(flow.width()), float(flow.height()));
//ORCA: Emit lightning infill per base area to avoid interleaving across islands.
if (m_support_params.base_fill_pattern == ipLightning) {
double print_z = ts_layer->print_z;
auto lightning_layer_mapping = printZ_to_lightninglayer.find(print_z);
if (lightning_layer_mapping != printZ_to_lightninglayer.end()) {
auto &lightning_layer = generator->getTreesForLayer(lightning_layer_mapping->second);
ExPolygons areas;
areas.emplace_back(poly);
areas = offset_ex(areas, -flow.scaled_spacing());
for (auto &area : areas) {
Polylines polylines = lightning_layer.convertToLines(to_polygons(area), 0);
for (auto itr = polylines.begin(); itr != polylines.end();) {
if (itr->length() < scale_(1.0))
itr = polylines.erase(itr);
else
itr++;
}
Polylines opt_polylines;
append(opt_polylines, chain_polylines(std::move(polylines)));
extrusion_entities_append_paths(base_dst, opt_polylines, erSupportMaterial,
float(flow.mm3_per_mm()), float(flow.width()), float(flow.height()));
#ifdef SUPPORT_TREE_DEBUG_TO_SVG
std::string name = debug_out_path("trees_polyline_%.2f.svg", ts_layer->print_z);
BoundingBox bbox = get_extents(ts_layer->base_areas);
SVG svg(name, bbox);
if (svg.is_opened()) {
svg.draw(ts_layer->base_areas, "blue");
svg.draw(generator->Overhangs()[printZ_to_lightninglayer[print_z]], "red");
for (auto &line : opt_polylines) svg.draw(line, "yellow");
}
std::string name = debug_out_path("trees_polyline_%.2f.svg", ts_layer->print_z);
BoundingBox bbox = get_extents(ts_layer->base_areas);
SVG svg(name, bbox);
if (svg.is_opened()) {
svg.draw(ts_layer->base_areas, "blue");
svg.draw(generator->Overhangs()[lightning_layer_mapping->second], "red");
for (auto &line : opt_polylines) svg.draw(line, "yellow");
}
#endif
}
}
}
//ORCA: Keep per-area base paths grouped for outline->fill preservation.
if (!base_eec->empty())
ts_layer->support_fills.entities.push_back(base_eec.release());
}
}
@@ -1696,13 +1687,6 @@ void TreeSupport::generate_toolpaths()
);
}
void deleteDirectoryContents(const std::filesystem::path& dir)
{
for (const auto& entry : std::filesystem::directory_iterator(dir))
std::filesystem::remove_all(entry.path());
}
void TreeSupport::move_bounds_to_contact_nodes(std::vector<TreeSupport3D::SupportElements> &move_bounds,
PrintObject &print_object,
const TreeSupport3D::TreeSupportSettings &config)
@@ -2155,13 +2139,9 @@ void TreeSupport::draw_circles()
if (!area.empty()) has_circle_node = true;
if (node.need_extra_wall) need_extra_wall = true;
// Merge the overhang into the roof area so tree tips can still produce
// a continuous support interface. Suppressing this for build-plate-only
// support drops the roof polygons entirely in valid tree branches.
// ORCA: Only keep top interface polygons that fully fit in the mm height cap.
if (top_interface_layers > 0 && node.support_roof_layers_below > 0 &&
(node.dist_mm_to_top - this->top_z_distance) < top_interface_height + EPSILON &&
!node.is_sharp_tail) {
// merge overhang to get a smoother interface surface
// Do not merge when buildplate_only is on, because some underneath nodes may have been deleted.
if (top_interface_layers > 0 && node.support_roof_layers_below > 0 && !on_buildplate_only && !node.is_sharp_tail) {
ExPolygons overhang_expanded;
if (node.overhang.contour.size() > 100 || node.overhang.holes.size()>1)
overhang_expanded.emplace_back(node.overhang);
@@ -2207,16 +2187,6 @@ void TreeSupport::draw_circles()
roof_1st_layer = diff_ex(roof_1st_layer, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roof_areas,get_extents(roof_1st_layer)));
roof_1st_layer = intersection_ex(roof_1st_layer, m_machine_border);
// Build-plate-only pruning can collapse the roof stack down to a single
// printable layer. In that case we still need to emit an interface layer
// instead of downgrading the last roof-adjacent layer to base support.
if (on_buildplate_only && top_interface_layers > 0 && roof_areas.empty() && !roof_1st_layer.empty()) {
append(roof_areas, roof_1st_layer);
roof_1st_layer.clear();
max_layers_above_roof = std::max(max_layers_above_roof, max_layers_above_roof1);
max_layers_above_roof1 = 0;
}
ExPolygons roofs; append(roofs, roof_1st_layer); append(roofs, roof_areas);append(roofs, roof_gap_areas);
base_areas = diff_ex(base_areas, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roofs, get_extents(base_areas)));
base_areas = intersection_ex(base_areas, m_machine_border);
@@ -3576,7 +3546,14 @@ void TreeSupport::generate_contact_points()
}
// add supports along contours
libnest2d::placers::EdgeCache<ExPolygon> edge_cache(overhang);
ExPolygon closed_overhang = overhang; // make a copy to add closing point for edge cache
if (closed_overhang.contour.points.size() > 1)
closed_overhang.contour.points.emplace_back(closed_overhang.contour.points.front());
for (Polygon &hole : closed_overhang.holes)
if (hole.points.size() > 1)
hole.points.emplace_back(hole.points.front());
libnest2d::placers::EdgeCache<ExPolygon> edge_cache(closed_overhang);
for (size_t i = 0; i < edge_cache.holeCount() + 1; i++) {
double step = point_spread / (i == 0 ? edge_cache.circumference() : edge_cache.circumference(i - 1));
double distance = 0;
+20 -15
View File
@@ -124,6 +124,9 @@ static std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> group_me
{
std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> grouped_meshes;
// Orca: Recompute static mesh-group state for this support generation pass.
TreeSupportSettings::zero_top_z_gap = false;
//FIXME this is ugly, it does not belong here.
for (size_t object_id : print_object_ids) {
const PrintObject &print_object = *print.get_object(object_id);
@@ -1600,13 +1603,19 @@ static Point move_inside_if_outside(const Polygons &polygons, Point from, int di
if (settings.increase_radius)
current_elem.effective_radius_height += 1;
coord_t radius = support_element_collision_radius(config, current_elem);
const auto _tiny_area_threshold = tiny_area_threshold();
if (settings.move) {
increased = relevant_offset;
if (overspeed > 0) {
const coord_t safe_movement_distance =
coord_t safe_movement_distance =
(current_elem.use_min_xy_dist ? config.xy_min_distance : config.xy_distance) +
(std::min(config.z_distance_top_layers, config.z_distance_bottom_layers) > 0 ? config.min_feature_size : 0);
// Orca:
// safe_movement_distance is used as the safe_offset_inc() step, so keep it non-zero
// to preserve branch movement with zero-clearance support settings.
if (safe_movement_distance == 0)
safe_movement_distance = scaled<coord_t>(0.1);
// The difference to ensure that the result not only conforms to wall_restriction, but collision/avoidance is done later.
// The higher last_safe_step_movement_distance comes exactly from the fact that the collision will be subtracted later.
increased = safe_offset_inc(increased, overspeed, volumes.getWallRestriction(support_element_collision_radius(config, parent.state), layer_idx, parent.state.use_min_xy_dist),
@@ -1817,9 +1826,15 @@ static void increase_areas_one_layer(
* layer z-1:dddddxxxxxxxxxx
* For more detailed visualisation see calculateWallRestrictions
*/
const coord_t safe_movement_distance =
coord_t safe_movement_distance =
(elem.use_min_xy_dist ? config.xy_min_distance : config.xy_distance) +
(std::min(config.z_distance_top_layers, config.z_distance_bottom_layers) > 0 ? config.min_feature_size : 0);
// safe_movement_distance is used as a divisor and as the safe_offset_inc() step,
// so keep it non-zero to avoid division by zero and preserve branch movement.
if (safe_movement_distance == 0)
safe_movement_distance = scaled<coord_t>(0.1);
if (ceiled_parent_radius == volumes.ceilRadius(projected_radius_increased, parent.state.use_min_xy_dist) ||
projected_radius_increased < config.increase_radius_until_radius)
// If it is guaranteed possible to increase the radius, the maximum movement speed can be increased, as it is assumed that the maximum movement speed is the one of the slower moving wall
@@ -3454,6 +3469,7 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons
// value is the area where support may be placed. As this is calculated in CreateLayerPathing it is saved and reused in draw_areas
std::vector<SupportElements> move_bounds(num_support_layers);
// ### Place tips of the support tree
for (size_t mesh_idx : processing.second)
generate_initial_areas(*print.get_object(mesh_idx), volumes, config, overhangs,
@@ -3764,6 +3780,7 @@ void organic_draw_branches(
// ++ ielement;
}
}
const SlicingParameters &slicing_params = print_object.slicing_parameters();
MeshSlicingParams mesh_slicing_params;
mesh_slicing_params.mode = MeshSlicingParams::SlicingMode::Positive;
@@ -3945,19 +3962,7 @@ void organic_draw_branches(
}
// ORCA: bottom contacts provide the footprint; interface layers are built later.
#if 0
//FIXME branch.has_tip seems to not be reliable.
if (branch.has_tip && interface_placer.support_parameters.has_top_contacts)
// Add top slices to top contacts / interfaces / base interfaces.
for (int i = int(branch.path.size()) - 1; i >= 0; -- i) {
const SupportElement &el = *branch.path[i];
if (el.state.missing_roof_layers == 0)
break;
//FIXME Move or not?
interface_placer.add_roof(std::move(slices[int(slices.size()) - i - 1]), el.state.layer_idx,
interface_placer.support_parameters.num_top_interface_layers + 1 - el.state.missing_roof_layers);
}
#endif
recover_pending_branch_roofs(interface_placer, branch.path, layer_begin, slices);
while (! slices.empty() && slices.back().empty()) {
slices.pop_back();
+1
View File
@@ -79,6 +79,7 @@ namespace boost { namespace filesystem { class directory_entry; }}
namespace Slic3r {
extern void set_logging_level(unsigned int level);
extern void set_logging_file(const std::string &file);
extern unsigned int level_string_to_boost(std::string level);
extern std::string get_string_logging_level(unsigned level);
extern unsigned get_logging_level();
+5
View File
@@ -129,6 +129,11 @@ void set_logging_level(unsigned int level)
);
}
void set_logging_file(const std::string &file)
{
boost::log::add_file_log(file);
}
unsigned int level_string_to_boost(std::string level)
{
std::map<std::string, int> Control_Param;
+4
View File
@@ -632,6 +632,8 @@ set(SLIC3R_GUI_SOURCES
Utils/SnapmakerPrinterAgent.hpp
Utils/MoonrakerPrinterAgent.cpp
Utils/MoonrakerPrinterAgent.hpp
Utils/Moonraker.cpp
Utils/Moonraker.hpp
Utils/BBLCloudServiceAgent.cpp
Utils/BBLCloudServiceAgent.hpp
Utils/BBLPrinterAgent.cpp
@@ -665,6 +667,8 @@ set(SLIC3R_GUI_SOURCES
Utils/UndoRedo.cpp
Utils/UndoRedo.hpp
Utils/WebSocketClient.hpp
Utils/3DPrinterOS.hpp
Utils/3DPrinterOS.cpp
Utils/WxFontUtils.cpp
Utils/WxFontUtils.hpp
Utils/FileTransferUtils.cpp
+3 -3
View File
@@ -998,10 +998,10 @@ float GLVolumeCollection::get_selection_support_normal_z() const
} else { // For normal supports, if the angle is set to 0, calculate normal_z from overlap.
const double layer_height = full_cfg.opt_float("layer_height");
const auto* nozzle_diameter_opt = full_cfg.option<ConfigOptionFloats>("nozzle_diameter");
const int wall_filament = full_cfg.opt_int("wall_filament");
const int wall_filament_id = full_cfg.opt_int("outer_wall_filament_id");
const size_t nozzle_count = nozzle_diameter_opt->values.size();
const size_t wall_extruder_idx = (wall_filament > 0 && wall_filament <= static_cast<int>(nozzle_count))
? static_cast<size_t>(wall_filament - 1)
const size_t wall_extruder_idx = (wall_filament_id > 0 && wall_filament_id <= static_cast<int>(nozzle_count))
? static_cast<size_t>(wall_filament_id - 1)
: 0; // Invalid extruder index falls back to extruder 1.
// Use wall extruder's nozzle diameter for better estimation of external perimeter width,
+11 -9
View File
@@ -592,9 +592,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
toggle_field(el, have_perimeters);
bool have_infill = config->option<ConfigOptionPercent>("sparse_infill_density")->value > 0;
// sparse_infill_filament uses the same logic as in Print::extruders()
// sparse_infill_filament_id uses the same logic as in Print::extruders()
for (auto el : { "sparse_infill_pattern", "infill_combination", "fill_multiline","infill_direction",
"minimum_sparse_infill_area", "sparse_infill_filament", "infill_anchor", "infill_anchor_max","infill_shift_step","sparse_infill_rotate_template","symmetric_infill_y_axis"})
"minimum_sparse_infill_area", "sparse_infill_filament_id", "infill_anchor", "infill_anchor_max","infill_shift_step","sparse_infill_rotate_template","symmetric_infill_y_axis"})
toggle_line(el, have_infill);
bool have_combined_infill = config->opt_bool("infill_combination") && have_infill;
@@ -655,8 +655,8 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
toggle_field("bottom_surface_density", has_bottom_shell);
for (auto el : { "infill_direction", "sparse_infill_line_width", "gap_fill_target","filter_out_gap_fill","infill_wall_overlap",
"sparse_infill_speed", "bridge_speed", "internal_bridge_speed", "bridge_angle", "internal_bridge_angle",
"solid_infill_direction", "solid_infill_rotate_template", "internal_solid_infill_pattern", "solid_infill_filament",
"sparse_infill_speed", "bridge_speed", "internal_bridge_speed", "bridge_angle", "internal_bridge_angle", "relative_bridge_angle",
"solid_infill_direction", "solid_infill_rotate_template", "internal_solid_infill_pattern", "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id",
})
toggle_field(el, have_infill || has_solid_infill);
@@ -711,8 +711,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
config->opt_enum<BrimType>("brim_type") != btPainted;
toggle_field("brim_width", have_brim_width);
toggle_field("brim_flow_ratio", have_brim);
// wall_filament uses the same logic as in Print::extruders()
toggle_field("wall_filament", have_perimeters || have_brim);
// Wall filament selectors use the same logic as in Print::extruders().
toggle_field("outer_wall_filament_id", have_perimeters || have_brim);
toggle_field("inner_wall_filament_id", have_perimeters || have_brim);
bool have_brim_ear = (config->opt_enum<BrimType>("brim_type") == btEar);
const auto brim_width = config->opt_float("brim_width");
@@ -836,9 +837,6 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
toggle_line("enable_tower_interface_cooldown_during_tower",
have_prime_tower && config->opt_bool("enable_tower_interface_features"));
for (auto el : {"wall_filament", "sparse_infill_filament", "solid_infill_filament", "wipe_tower_filament"})
toggle_line(el, !bSEMM);
bool purge_in_primetower = preset_bundle->printers.get_edited_preset().config.opt_bool("purge_in_prime_tower");
for (auto el : {"wipe_tower_rotation_angle", "wipe_tower_cone_angle",
@@ -964,6 +962,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
bool lattice_options = config->opt_enum<InfillPattern>("sparse_infill_pattern") == InfillPattern::ipLateralLattice;
for (auto el : { "lateral_lattice_angle_1", "lateral_lattice_angle_2"})
toggle_line(el, lattice_options);
bool lightning_options = config->opt_enum<InfillPattern>("sparse_infill_pattern") == InfillPattern::ipLightning;
for (auto el : { "lightning_overhang_angle", "lightning_prune_angle", "lightning_straightening_angle" })
toggle_line(el, lightning_options);
// Adaptative Cubic and support cubic infill patterns do not support infill rotation.
bool FillAdaptive = (pattern == InfillPattern::ipAdaptiveCubic || pattern == InfillPattern::ipSupportCubic);
+2 -1
View File
@@ -190,6 +190,7 @@ public:
/// Call the attached m_fn_edit_value method.
void on_edit_value();
virtual void propagate_value(){}
public:
/// parent wx item, opportunity to refactor (probably not necessary - data duplication)
wxWindow* m_parent {nullptr};
@@ -317,7 +318,7 @@ public:
void BUILD() override;
bool value_was_changed();
// Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER
void propagate_value();
virtual void propagate_value() override;
wxWindow* window {nullptr};
void set_value(const std::string& value, bool change_event = false) {
+403 -8
View File
@@ -1218,10 +1218,22 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed)
GLCanvas3D::~GLCanvas3D()
{
if (m_fxaa_texture_id != 0 && _set_current()) {
glsafe(::glDeleteTextures(1, &m_fxaa_texture_id));
m_fxaa_texture_id = 0;
if (_set_current()) {
if (m_fxaa_texture_id != 0) {
glsafe(::glDeleteTextures(1, &m_fxaa_texture_id));
m_fxaa_texture_id = 0;
}
if (m_ssao_color_texture_id != 0) {
glsafe(::glDeleteTextures(1, &m_ssao_color_texture_id));
m_ssao_color_texture_id = 0;
}
if (m_ssao_depth_texture_id != 0) {
glsafe(::glDeleteTextures(1, &m_ssao_depth_texture_id));
m_ssao_depth_texture_id = 0;
}
m_plate_shadow_mask.reset();
}
m_plate_shadow_mask_key.clear();
reset_volumes();
@@ -2039,14 +2051,16 @@ void GLCanvas3D::render(bool only_init)
/* view3D render*/
int hover_id = (m_hover_plate_idxs.size() > 0)?m_hover_plate_idxs.front():-1;
if (m_canvas_type == ECanvasType::CanvasView3D) {
//BBS: add outline logic
_render_objects(GLVolumeCollection::ERenderType::Opaque, !m_gizmos.is_running());
_render_sla_slices();
_render_selection();
if (!no_partplate)
_render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), m_show_world_axes);
if (!no_partplate) //BBS: add outline logic
_render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, only_body, hover_id, true, show_grid);
//BBS: add outline logic
_render_cast_shadows_on_plate(camera.get_view_matrix(), camera.get_projection_matrix());
_render_objects(GLVolumeCollection::ERenderType::Opaque, !m_gizmos.is_running());
_render_sla_slices();
_render_selection();
_render_objects(GLVolumeCollection::ERenderType::Transparent, !m_gizmos.is_running());
}
/* preview render */
@@ -2103,6 +2117,9 @@ void GLCanvas3D::render(bool only_init)
if (m_picking_enabled && m_rectangle_selection.is_dragging())
m_rectangle_selection.render(*this);
if (_is_ssao_enabled())
_render_ssao_pass(static_cast<unsigned int>(cnv_size.get_width()), static_cast<unsigned int>(cnv_size.get_height()));
if (_is_fxaa_enabled())
_render_fxaa_pass(static_cast<unsigned int>(cnv_size.get_width()), static_cast<unsigned int>(cnv_size.get_height()));
@@ -7502,6 +7519,14 @@ bool GLCanvas3D::_is_fxaa_enabled() const
return wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_FXAA_ENABLED);
}
bool GLCanvas3D::_is_ssao_enabled() const
{
if (wxGetApp().app_config == nullptr)
return false;
return wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_MODE) &&
wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SSAO);
}
int GLCanvas3D::_get_effective_fps_cap() const
{
if (wxGetApp().app_config == nullptr)
@@ -7596,6 +7621,159 @@ void GLCanvas3D::_render_fxaa_pass(unsigned int width, unsigned int height)
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
}
void GLCanvas3D::_render_ssao_pass(unsigned int width, unsigned int height)
{
if (width == 0 || height == 0)
return;
GLShaderProgram* shader = wxGetApp().get_shader("ssao");
if (shader == nullptr)
return;
if (m_ssao_color_texture_id == 0) {
glsafe(::glGenTextures(1, &m_ssao_color_texture_id));
glsafe(::glBindTexture(GL_TEXTURE_2D, m_ssao_color_texture_id));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));
}
if (m_ssao_depth_texture_id == 0) {
glsafe(::glGenTextures(1, &m_ssao_depth_texture_id));
glsafe(::glBindTexture(GL_TEXTURE_2D, m_ssao_depth_texture_id));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));
}
if (m_ssao_texture_size[0] != width || m_ssao_texture_size[1] != height) {
glsafe(::glBindTexture(GL_TEXTURE_2D, m_ssao_color_texture_id));
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr));
glsafe(::glBindTexture(GL_TEXTURE_2D, m_ssao_depth_texture_id));
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, width, height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, nullptr));
m_ssao_texture_size = { { width, height } };
}
glsafe(::glBindTexture(GL_TEXTURE_2D, m_ssao_color_texture_id));
glsafe(::glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, width, height));
glsafe(::glBindTexture(GL_TEXTURE_2D, m_ssao_depth_texture_id));
glsafe(::glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, width, height));
const Camera& camera = wxGetApp().plater()->get_camera();
GLint prev_stencil_mask = 0xFF;
glsafe(::glGetIntegerv(GL_STENCIL_WRITEMASK, &prev_stencil_mask));
GLboolean prev_stencil_test = GL_FALSE;
glsafe(::glGetBooleanv(GL_STENCIL_TEST, &prev_stencil_test));
GLboolean prev_depth_mask = GL_TRUE;
glsafe(::glGetBooleanv(GL_DEPTH_WRITEMASK, &prev_depth_mask));
GLint prev_depth_func = GL_LESS;
glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func));
glsafe(::glDisable(GL_DEPTH_TEST));
glsafe(::glDisable(GL_BLEND));
// Build stencil mask for bed/plate and apply SSAO only outside this mask.
glsafe(::glEnable(GL_STENCIL_TEST));
glsafe(::glStencilMask(0xFF));
glsafe(::glClearStencil(0));
glsafe(::glClear(GL_STENCIL_BUFFER_BIT));
glsafe(::glStencilFunc(GL_ALWAYS, 1, 0xFF));
glsafe(::glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE));
// Mark only visible plate pixels (do not exclude objects in front of plate).
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glDepthMask(GL_FALSE));
glsafe(::glDepthFunc(GL_LEQUAL));
GLboolean prev_color_mask[4] = { GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE };
glsafe(::glGetBooleanv(GL_COLOR_WRITEMASK, prev_color_mask));
glsafe(::glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE));
if (const BuildVolume& build_volume = m_bed.build_volume(); build_volume.valid()) {
GLShaderProgram* flat = wxGetApp().get_shader("flat");
if (flat != nullptr) {
flat->start_using();
flat->set_uniform("projection_matrix", camera.get_projection_matrix());
GLModel plate_mask;
GLModel::Geometry mask;
mask.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
if (build_volume.type() == BuildVolume_Type::Rectangle) {
const BoundingBox3Base<Vec3d> bb = build_volume.bounding_volume();
mask.reserve_vertices(4);
mask.reserve_indices(6);
mask.add_vertex(Vec3f((float)bb.min.x(), (float)bb.min.y(), 0.0f));
mask.add_vertex(Vec3f((float)bb.max.x(), (float)bb.min.y(), 0.0f));
mask.add_vertex(Vec3f((float)bb.max.x(), (float)bb.max.y(), 0.0f));
mask.add_vertex(Vec3f((float)bb.min.x(), (float)bb.max.y(), 0.0f));
mask.add_triangle(0, 1, 2);
mask.add_triangle(0, 2, 3);
} else if (build_volume.type() == BuildVolume_Type::Circle) {
const Vec2f c = Vec2f(unscaled<float>(build_volume.circle().center.x()), unscaled<float>(build_volume.circle().center.y()));
const float r = unscaled<float>(build_volume.circle().radius);
const int segments = 64;
mask.reserve_vertices(segments + 1);
mask.reserve_indices(segments * 3);
mask.add_vertex(Vec3f(c.x(), c.y(), 0.0f));
for (int i = 0; i < segments; ++i) {
const float a = (2.0f * float(PI) * float(i)) / float(segments);
mask.add_vertex(Vec3f(c.x() + r * std::cos(a), c.y() + r * std::sin(a), 0.0f));
}
for (int i = 0; i < segments; ++i) {
const unsigned int i1 = 1 + i;
const unsigned int i2 = 1 + ((i + 1) % segments);
mask.add_triangle(0, i1, i2);
}
}
if (mask.vertices_count() > 0 && mask.indices_count() > 0) {
plate_mask.init_from(std::move(mask));
flat->set_uniform("view_model_matrix", camera.get_view_matrix());
plate_mask.render(flat);
}
flat->stop_using();
}
}
glsafe(::glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE));
glsafe(::glDisable(GL_DEPTH_TEST));
glsafe(::glStencilMask(0x00));
glsafe(::glStencilFunc(GL_NOTEQUAL, 1, 0xFF));
glsafe(::glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP));
shader->start_using();
shader->set_uniform("view_model_matrix", Transform3d::Identity());
shader->set_uniform("projection_matrix", Transform3d::Identity());
shader->set_uniform("color_texture", 0);
shader->set_uniform("depth_texture", 1);
shader->set_uniform("inv_tex_size", Vec2f(1.0f / static_cast<float>(width), 1.0f / static_cast<float>(height)));
shader->set_uniform("z_near", camera.get_near_z());
shader->set_uniform("z_far", camera.get_far_z());
glsafe(::glActiveTexture(GL_TEXTURE0));
glsafe(::glBindTexture(GL_TEXTURE_2D, m_ssao_color_texture_id));
glsafe(::glActiveTexture(GL_TEXTURE1));
glsafe(::glBindTexture(GL_TEXTURE_2D, m_ssao_depth_texture_id));
m_background.render();
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
glsafe(::glActiveTexture(GL_TEXTURE0));
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
shader->stop_using();
if (!prev_stencil_test)
glsafe(::glDisable(GL_STENCIL_TEST));
glsafe(::glStencilMask(prev_stencil_mask));
glsafe(::glColorMask(prev_color_mask[0], prev_color_mask[1], prev_color_mask[2], prev_color_mask[3]));
glsafe(::glDepthMask(prev_depth_mask));
glsafe(::glDepthFunc(prev_depth_func));
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
}
void GLCanvas3D::_render_background()
{
bool use_error_color = false;
@@ -7686,6 +7864,206 @@ void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transfo
wxGetApp().plater()->get_partplate_list().render(view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid);
}
void GLCanvas3D::_render_cast_shadows_on_plate(const Transform3d& view_matrix, const Transform3d& projection_matrix)
{
// Check if shadow rendering is enabled in configuration
if (wxGetApp().app_config == nullptr)
return;
if (!wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_MODE))
return;
if (!wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS))
return;
if (m_volumes.empty())
return;
GLShaderProgram* shader = wxGetApp().get_shader("flat");
if (shader == nullptr)
return;
// Fixed light direction (pointing downward at an angle)
// Drive shadow direction from current view angle: define light in eye-space,
// then transform it to world-space with inverse view rotation.
const Vec3d light_dir_eye = Vec3d(-0.4574957, 0.4574957, 0.7624929).normalized();
const Matrix3d view_rot = view_matrix.matrix().block<3, 3>(0, 0);
const Vec3d light_dir_to_light = (view_rot.transpose() * light_dir_eye).normalized();
const Vec3d ray_dir = -light_dir_to_light; // Direction of shadow projection
if (std::abs(ray_dir.z()) < 1e-6)
return;
// Shadow projection matrix - flattens geometry onto Z=0 plane along light direction
Matrix4d shadow_proj = Matrix4d::Identity();
shadow_proj(0, 2) = -ray_dir.x() / ray_dir.z();
shadow_proj(1, 2) = -ray_dir.y() / ray_dir.z();
shadow_proj(2, 0) = 0.0;
shadow_proj(2, 1) = 0.0;
shadow_proj(2, 2) = 0.0;
shadow_proj(2, 3) = 0.01; // Bias to prevent shadow acne
// Save OpenGL state
GLint prev_depth_func = GL_LESS;
glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func));
GLboolean prev_depth_mask = GL_TRUE;
glsafe(::glGetBooleanv(GL_DEPTH_WRITEMASK, &prev_depth_mask));
GLint prev_stencil_mask = 0xFF;
glsafe(::glGetIntegerv(GL_STENCIL_WRITEMASK, &prev_stencil_mask));
GLboolean prev_stencil_test = GL_FALSE;
glsafe(::glGetBooleanv(GL_STENCIL_TEST, &prev_stencil_test));
// ============================================================
// PASS 0: Create stencil mask for the build plate (value = 1)
// ============================================================
glsafe(::glEnable(GL_STENCIL_TEST));
glsafe(::glStencilMask(0xFF));
glsafe(::glClearStencil(0));
glsafe(::glClear(GL_STENCIL_BUFFER_BIT));
glsafe(::glStencilFunc(GL_ALWAYS, 1, 0xFF));
glsafe(::glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE));
glsafe(::glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE));
glsafe(::glDisable(GL_DEPTH_TEST));
shader->start_using();
shader->set_uniform("projection_matrix", projection_matrix);
// Draw the build plate (cached model to avoid per-frame uploads)
if (const BuildVolume& build_volume = m_bed.build_volume(); build_volume.valid()) {
const std::string mask_key = build_volume.type() == BuildVolume_Type::Rectangle
? (boost::format("rect|%1$.5f|%2$.5f|%3$.5f|%4$.5f")
% build_volume.bounding_volume().min.x()
% build_volume.bounding_volume().min.y()
% build_volume.bounding_volume().max.x()
% build_volume.bounding_volume().max.y()).str()
: (build_volume.type() == BuildVolume_Type::Circle
? (boost::format("circle|%1$.5f|%2$.5f|%3$.5f")
% unscaled<double>(build_volume.circle().center.x())
% unscaled<double>(build_volume.circle().center.y())
% unscaled<double>(build_volume.circle().radius)).str()
: std::string("invalid"));
if (mask_key != m_plate_shadow_mask_key) {
m_plate_shadow_mask.reset();
m_plate_shadow_mask_key = mask_key;
GLModel::Geometry mask;
mask.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
if (build_volume.type() == BuildVolume_Type::Rectangle) {
const BoundingBox3Base<Vec3d> bb = build_volume.bounding_volume();
mask.reserve_vertices(4);
mask.reserve_indices(6);
mask.add_vertex(Vec3f((float)bb.min.x(), (float)bb.min.y(), 0.0f));
mask.add_vertex(Vec3f((float)bb.max.x(), (float)bb.min.y(), 0.0f));
mask.add_vertex(Vec3f((float)bb.max.x(), (float)bb.max.y(), 0.0f));
mask.add_vertex(Vec3f((float)bb.min.x(), (float)bb.max.y(), 0.0f));
mask.add_triangle(0, 1, 2);
mask.add_triangle(0, 2, 3);
}
else if (build_volume.type() == BuildVolume_Type::Circle) {
const Vec2f c = Vec2f(unscaled<float>(build_volume.circle().center.x()), unscaled<float>(build_volume.circle().center.y()));
const float r = unscaled<float>(build_volume.circle().radius);
const int segments = 64;
mask.reserve_vertices(segments + 1);
mask.reserve_indices(segments * 3);
mask.add_vertex(Vec3f(c.x(), c.y(), 0.0f));
for (int i = 0; i < segments; ++i) {
const float a = (2.0f * float(PI) * float(i)) / float(segments);
mask.add_vertex(Vec3f(c.x() + r * std::cos(a), c.y() + r * std::sin(a), 0.0f));
}
for (int i = 0; i < segments; ++i) {
const unsigned int i1 = 1 + i;
const unsigned int i2 = 1 + ((i + 1) % segments);
mask.add_triangle(0, i1, i2);
}
}
if (mask.vertices_count() > 0 && mask.indices_count() > 0)
m_plate_shadow_mask.init_from(std::move(mask));
}
if (m_plate_shadow_mask.is_initialized()) {
shader->set_uniform("view_model_matrix", view_matrix);
m_plate_shadow_mask.render(shader);
}
}
// ============================================================
// PASS 1: Project object shadows onto plate (increment stencil to 2)
// ============================================================
// Only render where plate exists (stencil == 1), then increment to 2
glsafe(::glStencilFunc(GL_EQUAL, 1, 0xFF));
glsafe(::glStencilOp(GL_KEEP, GL_KEEP, GL_INCR));
glsafe(::glDepthMask(GL_FALSE));
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glDepthFunc(GL_ALWAYS)); // Shadows don't need depth testing
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-2.0f, -2.0f));
glsafe(::glDisable(GL_CULL_FACE));
// Render projected shadow geometry
for (GLVolume* volume : m_volumes.volumes) {
if (volume == nullptr || !volume->is_active || !volume->printable || volume->is_modifier || volume->is_wipe_tower)
continue;
// CRITICAL FIX: Apply shadow projection in object's local space, then to world, then to view
// This ensures shadows are cast from the object's actual position
Matrix4d world_matrix = volume->world_matrix().matrix();
// Project the shadow - this flattens the geometry onto Z=0 in WORLD space
Matrix4d shadow_world_matrix = shadow_proj * world_matrix;
// Transform to view space for rendering
Matrix4d view_shadow_matrix = view_matrix.matrix() * shadow_world_matrix;
shader->set_uniform("view_model_matrix", view_shadow_matrix);
shader->set_uniform("projection_matrix", projection_matrix);
volume->model.render(shader);
}
// ============================================================
// PASS 2: Draw shadow color where stencil == 2
// ============================================================
glsafe(::glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE));
glsafe(::glStencilFunc(GL_EQUAL, 2, 0xFF));
glsafe(::glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP));
glsafe(::glStencilMask(0x00));
glsafe(::glDepthFunc(GL_ALWAYS));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
// Draw shadow fill
shader->set_uniform("view_model_matrix", Transform3d::Identity());
shader->set_uniform("projection_matrix", Transform3d::Identity());
const ColorRGBA shadow_fill_color(0.0f, 0.0f, 0.0f, 0.4f); // Darker shadow for visibility
const ColorRGBA prev_bg_color = m_background.get_geometry().color;
m_background.set_color(shadow_fill_color);
shader->set_uniform("uniform_color", shadow_fill_color);
m_background.render(shader);
m_background.set_color(prev_bg_color);
shader->set_uniform("uniform_color", prev_bg_color);
shader->stop_using();
// ============================================================
// RESTORE STATE
// ============================================================
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glDepthMask(prev_depth_mask));
glsafe(::glDepthFunc(prev_depth_func));
glsafe(::glEnable(GL_CULL_FACE));
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
glsafe(::glDisable(GL_BLEND));
if (!prev_stencil_test)
glsafe(::glDisable(GL_STENCIL_TEST));
glsafe(::glStencilMask(prev_stencil_mask));
}
void GLCanvas3D::_render_plane() const
{
;//TODO render assemble plane
@@ -7756,12 +8134,20 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with
else
m_volumes.set_show_sinking_contours(!m_gizmos.is_hiding_instances());
GLShaderProgram* shader = wxGetApp().get_shader("gouraud");
const bool realistic_mode = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_MODE);
const bool realistic_phong = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_PHONG);
const std::string shader_name = (realistic_mode && realistic_phong) ? "phong" : "gouraud";
GLShaderProgram* shader = wxGetApp().get_shader(shader_name);
if (shader == nullptr && shader_name != "gouraud")
shader = wxGetApp().get_shader("gouraud");
ECanvasType canvas_type = this->m_canvas_type;
bool partly_inside_enable = canvas_type == ECanvasType::CanvasAssembleView ? false : true;
if (shader != nullptr) {
shader->start_using();
const bool phong_ssao = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SSAO);
shader->set_uniform("enable_ssao", phong_ssao);
const Size& cvn_size = get_canvas_size();
{
const Camera& camera = wxGetApp().plater()->get_camera();
@@ -8836,6 +9222,15 @@ void GLCanvas3D::_render_canvas_toolbar()
[this]{wxGetApp().toggle_show_outline();}
);
create_menu_item( _utf8(L("Realistic View")),
true,
cfg->get_bool(SETTING_OPENGL_REALISTIC_MODE),
[this, &cfg]{
cfg->set_bool(SETTING_OPENGL_REALISTIC_MODE, !cfg->get_bool(SETTING_OPENGL_REALISTIC_MODE));
cfg->save();
}
);
ImGui::Separator();
create_menu_item( _utf8(L("Perspective")),
+8
View File
@@ -727,6 +727,11 @@ public:
GLModel m_background;
unsigned int m_fxaa_texture_id{ 0 };
std::array<unsigned int, 2> m_fxaa_texture_size{ 0, 0 };
unsigned int m_ssao_color_texture_id{ 0 };
unsigned int m_ssao_depth_texture_id{ 0 };
std::array<unsigned int, 2> m_ssao_texture_size{ { 0, 0 } };
GLModel m_plate_shadow_mask;
std::string m_plate_shadow_mask_key;
public:
explicit GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed);
~GLCanvas3D();
@@ -1238,12 +1243,15 @@ private:
void _picking_pass();
void _rectangular_selection_picking_pass();
bool _is_fxaa_enabled() const;
bool _is_ssao_enabled() const;
int _get_effective_fps_cap() const;
bool _is_fps_overlay_enabled() const;
void _render_fps_overlay(int fps) const;
void _render_fxaa_pass(unsigned int width, unsigned int height);
void _render_ssao_pass(unsigned int width, unsigned int height);
void _render_background();
void _render_bed(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_axes);
void _render_cast_shadows_on_plate(const Transform3d& view_matrix, const Transform3d& projection_matrix);
//BBS: add part plate related logic
void _render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
//BBS: add outline drawing logic
+8
View File
@@ -50,6 +50,8 @@ std::pair<bool, std::string> GLShadersManager::init()
valid &= append_shader("flat_texture", { prefix + "flat_texture.vs", prefix + "flat_texture.fs" });
// used to apply post-processing antialiasing in screen space
valid &= append_shader("fxaa", { prefix + "fxaa.vs", prefix + "fxaa.fs" });
// used to apply screen-space ambient occlusion in post process
valid &= append_shader("ssao", { prefix + "ssao.vs", prefix + "ssao.fs" });
// used to render 3D scene background
valid &= append_shader("background", { prefix + "background.vs", prefix + "background.fs" });
#if SLIC3R_OPENGL_ES
@@ -76,6 +78,12 @@ std::pair<bool, std::string> GLShadersManager::init()
valid &= append_shader("gouraud", { prefix + "gouraud.vs", prefix + "gouraud.fs" }
#if ENABLE_ENVIRONMENT_MAP
, { "ENABLE_ENVIRONMENT_MAP"sv }
#endif // ENABLE_ENVIRONMENT_MAP
);
// used to render objects in 3d editor with phong shading
valid &= append_shader("phong", { prefix + "phong.vs", prefix + "phong.fs" }
#if ENABLE_ENVIRONMENT_MAP
, { "ENABLE_ENVIRONMENT_MAP"sv }
#endif // ENABLE_ENVIRONMENT_MAP
);
// used to render variable layers heights in 3d editor
+185 -36
View File
@@ -12,6 +12,7 @@
#include <boost/chrono/duration.hpp>
#include <boost/log/detail/native_typeof.hpp>
#include <libslic3r/Config.hpp>
#include <mutex>
#include <wx/event.h>
// Localization headers: include libslic3r version first so everything in this file
@@ -60,6 +61,7 @@
#include <wx/dialog.h>
#include <wx/textctrl.h>
#include <wx/splash.h>
#include <wx/weakref.h>
#include <wx/fontutil.h>
#include <wx/glcanvas.h>
#include <wx/utils.h>
@@ -348,6 +350,17 @@ public:
}
}
// Orca: keep the splash alive until it is explicitly destroyed.
// wxSplashScreen installs an application-wide event filter that calls
// Close() (which Destroy()s the window) on ANY key press or mouse-button
// down. Since startup keeps the splash up across the whole load_presets()
// and main-window-creation phase, a single stray click/keypress would
// destroy it while on_init_inner() still holds the pointer, causing an
// intermittent use-after-free crash. Override the filter to a no-op so the
// splash can only be removed via the explicit Destroy() once the main frame
// is shown.
int FilterEvent(wxEvent& /*event*/) override { return wxEventFilter::Event_Skip; }
void scale_font(wxFont& font, float scale)
{
#ifdef __WXMSW__
@@ -2762,7 +2775,8 @@ bool GUI_App::on_init_inner()
app_config->set("version", SLIC3R_VERSION);
}
SplashScreen * scrn = nullptr;
// Orca: use wxWeakRef to provent wild pointer.
wxWeakRef<SplashScreen> scrn = nullptr;
if (app_config->get("show_splash_screen") == "true") {
// Detect position (display) to show the splash screen
// Now this position is equal to the mainframe position
@@ -2981,7 +2995,11 @@ bool GUI_App::on_init_inner()
}
#endif
if (scrn) { scrn->SetText(_L("Creating main window") + dots); wxYield(); }
if (scrn) {
const auto scrn_txt = _L("Creating main window") + dots;
scrn->SetText(scrn_txt);
wxYield();
}
BOOST_LOG_TRIVIAL(info) << "create the main window";
mainframe = new MainFrame();
// hide settings tabs after first Layout
@@ -3349,6 +3367,8 @@ bool GUI_App::on_init_network(bool try_backup)
std::string country_code = app_config->get_country_code();
m_agent->set_country_code(country_code);
m_agent->start();
// Orca: disable Bambu telemetry up-front (before any login) so it never starts.
check_track_enable();
}
// When using Orca cloud alongside the BBL network plugin, the BBL DLL agent still
@@ -3365,6 +3385,12 @@ bool GUI_App::on_init_network(bool try_backup)
bbl.init_log();
bbl.set_cert_file(resources_dir() + "/cert", "slicer_base64.cer");
bbl.set_country_code(app_config->get_country_code());
// Orca: disable Bambu telemetry before start() so the DLL never spins up tracking
// workers. This covers the case where the BBL plugin is loaded for LAN discovery
// but the user has not registered BBL_CLOUD_PROVIDER (so m_agent->track_enable
// would not reach this DLL instance).
bbl.track_enable(false);
bbl.track_remove_files();
bbl.start();
}
}
@@ -4496,21 +4522,46 @@ std::string GUI_App::handle_web_request(std::string cmd)
boost::optional<std::string> command = root.get_optional<std::string>("command");
if (command.has_value()) {
std::string command_str = command.value();
static const std::unordered_set<std::string> stealth_blocked_commands = {
static const std::unordered_set<std::string> stealth_blocked_info_commands = {
"get_login_info",
"get_orca_login_info",
"get_bambu_login_info",
};
static const std::unordered_set<std::string> stealth_blocked_login_commands = {
"homepage_login_or_register",
"homepage_orca_login_or_register",
"homepage_bambu_login_or_register",
};
if (app_config->get_stealth_mode() && stealth_blocked_commands.count(command_str)) {
if (app_config->get_stealth_mode() && stealth_blocked_info_commands.count(command_str)) {
CallAfter([this] {
if (mainframe && mainframe->m_webview)
mainframe->m_webview->SendCloudProvidersInfo();
});
return "";
}
if (app_config->get_stealth_mode() && stealth_blocked_login_commands.count(command_str)) {
CallAfter([this, command_str] {
MessageDialog dlg(mainframe,
_L("You are currently in Stealth Mode. To log into the Cloud, you need to disable Stealth Mode first."),
_L("Stealth Mode"),
wxOK | wxCANCEL | wxCENTRE);
dlg.SetButtonLabel(wxID_OK, _L("Quit Stealth Mode"));
if (dlg.ShowModal() == wxID_OK) {
app_config->set_bool("stealth_mode", false);
app_config->save();
if (mainframe && mainframe->m_webview)
mainframe->m_webview->SendCloudProvidersInfo();
// Continue with login
if (command_str == "homepage_login_or_register")
this->request_login(true);
else if (command_str == "homepage_orca_login_or_register")
this->request_login(true, ORCA_CLOUD_PROVIDER);
else if (command_str == "homepage_bambu_login_or_register")
this->request_login(true, BBL_CLOUD_PROVIDER);
}
});
return "";
}
if (command_str.compare("request_project_download") == 0) {
if (root.get_child_optional("data") != boost::none) {
pt::ptree data_node = root.get_child("data");
@@ -4800,6 +4851,8 @@ void GUI_App::handle_http_error(unsigned int status, std::string body, const std
wxQueueEvent(this, evt);
}
static std::mutex conflict_ids_mutex;
void GUI_App::on_http_error(wxCommandEvent &evt)
{
int status = evt.GetInt();
@@ -4875,32 +4928,62 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
return;
}
static bool m_is_error_shown = false;
if (status == 409 && provider == ORCA_CLOUD_PROVIDER) {
BOOST_LOG_TRIVIAL(info) << "Http error 409.";
// Parse the conflict body to extract the error code and server profile id
int conflict_code = 0;
std::string conflict_setting_id;
try {
json conflict_body = json::parse(body_str);
if (conflict_body.contains("code"))
conflict_code = conflict_body["code"].get<int>();
if (conflict_body.contains("server_profile") && conflict_body["server_profile"].contains("id")
&& conflict_body["server_profile"]["id"].is_string())
conflict_setting_id = conflict_body["server_profile"]["id"].get<std::string>();
} catch (...) {
BOOST_LOG_TRIVIAL(warning) << "Failed to parse 409 conflict body.";
}
auto* plater = wxGetApp().plater();
if (plater != nullptr && wxGetApp().imgui()->display_initialized()) {
std::string text;
if (conflict_code == -1) {
text = _u8L("Cloud sync conflict: this preset has a newer version in OrcaCloud.\n"
"Pull downloads the cloud copy. Force push overwrites it with your local preset.");
} else {
text = _u8L("Cloud sync conflict: a preset with this name already exists in OrcaCloud.\n"
"Pull downloads the cloud copy. Force push overwrites it with your local preset.");
}
plater->get_notification_manager()->push_orca_sync_conflict_notification(
text,
[this](wxEvtHandler*) {
// Runs on the GUI thread (on_http_error is a queued wx event); restart_sync_user_preset()
// already joins the old sync thread off the UI thread, so no extra thread is needed here.
if (is_closing() || !m_agent || !preset_bundle)
return false;
BOOST_LOG_TRIVIAL(info) << "Pulling Orca Cloud settings to resolve sync conflict.";
restart_sync_user_preset();
return true;
},
[this, conflict_setting_id](wxEvtHandler*) {
if (mainframe == nullptr)
return false;
MessageDialog
dlg(mainframe,
_L("Force push will overwrite the cloud copy with your local preset changes.\nDo you want to continue?"),
_L("Resolve cloud sync conflict"), wxCENTER | wxYES_NO | wxNO_DEFAULT | wxICON_WARNING);
if (dlg.ShowModal() != wxID_YES)
return false;
force_push_conflicting_preset(conflict_setting_id);
return true;
});
}
return;
}
// Show general error notification for Orca Cloud API failures (not Bambu)
if (provider == ORCA_CLOUD_PROVIDER && status >= 400 && code != HttpErrorVersionLimited) {
wxString msg;
if (!error.empty()) {
msg = wxString::Format(_L("Failed to connect to OrcaCloud.\nPlease check your network connectivity\n(HTTP %u): %s"), status, wxString::FromUTF8(error));
} else {
msg = wxString::Format(_L("Failed to connect to OrcaCloud.\nPlease check your network connectivity\n(HTTP %u)"), status);
}
if (app_config->get_bool("developer_mode")) {
// Use notification manager if ImGui is ready; fall back to wxMessageBox on Linux
// where ImGui may not be initialized until the user switches to the Prepare tab.
if (wxGetApp().plater() != nullptr && wxGetApp().imgui()->display_initialized()) {
wxGetApp()
.plater()
->get_notification_manager()
->push_notification(NotificationType::PlaterError, NotificationManager::NotificationLevel::WarningNotificationLevel,
msg.ToUTF8().data());
}
}
if (!m_is_error_shown) {
m_is_error_shown = true;
wxMessageBox(msg, _L("Cloud Error"), wxOK | wxICON_ERROR, wxGetApp().mainframe);
}
BOOST_LOG_TRIVIAL(warning) << "API call to OrcaCloud failed with status=" << status;
}
}
@@ -4970,7 +5053,7 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
void GUI_App::check_track_enable()
{
// Orca: alaways disable track event
// Orca: telemetry only exists on the BBL cloud agent; always disable it.
if (m_agent) {
m_agent->track_enable(false);
m_agent->track_remove_files();
@@ -5974,6 +6057,25 @@ bool GUI_App::maybe_migrate_user_presets_on_login()
if (ret != 0) {
BOOST_LOG_TRIVIAL(warning) << "Failed to query OrcaCloud presets (error " << ret
<< "), skipping migration to avoid overwriting cloud data.";
// If this looks like a transient 401 from token propagation delay (within grace period),
// schedule one deferred retry so first-time users don't silently lose their preset migration.
if (std::chrono::steady_clock::now() - m_last_401_error_time < std::chrono::seconds(30)
&& !m_migration_retry_pending.exchange(true)) {
BOOST_LOG_TRIVIAL(info) << "Scheduling migration retry after token propagation window.";
boost::thread([this]() {
std::this_thread::sleep_for(std::chrono::seconds(5));
CallAfter([this]() {
m_migration_retry_pending = false;
if (is_closing() || !m_agent || !m_agent->is_user_login()) return;
BOOST_LOG_TRIVIAL(info) << "Retrying preset migration after token propagation window.";
if (maybe_migrate_user_presets_on_login()) {
const std::string user_id = m_agent->get_user_id();
preset_bundle->load_user_presets(user_id, ForwardCompatibilitySubstitutionRule::Enable);
if (mainframe) mainframe->update_side_preset_ui();
}
});
}).detach();
}
return false;
}
BOOST_LOG_TRIVIAL(info) << "OrcaCloud has no presets for user " << new_user_id << ", proceeding with migration check.";
@@ -6169,13 +6271,14 @@ void GUI_App::load_pending_vendors()
need_add_filaments.clear();
}
void GUI_App::sync_preset(Preset* preset)
void GUI_App::sync_preset(Preset* preset, bool force)
{
int result = -1;
unsigned int http_code = 200;
std::string updated_info;
long long update_time = 0;
// only sync user's preset
if (!m_agent) return;
if (!preset->is_user()) return;
auto setting_id = preset->setting_id;
@@ -6247,9 +6350,9 @@ void GUI_App::sync_preset(Preset* preset)
result = 0;
}
else {
result = m_agent->put_setting(setting_id, preset->name, &values_map, &http_code);
result = m_agent->put_setting(setting_id, preset->name, &values_map, &http_code, ORCA_CLOUD_PROVIDER, force);
if (http_code >= 400) {
result = 0;
result = 0;
updated_info = "hold";
BOOST_LOG_TRIVIAL(error) << "[sync_preset] put setting_id = " << setting_id << " failed, http_code = " << http_code;
} else {
@@ -6710,7 +6813,8 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
// Sync once immediately, then every 60 seconds.
while (!t.expired()) {
++tick_tock;
if (tick_tock % 120 == 0) {
// Sync once immediately, then every 60s, or right away when a force-push asked for it.
if (tick_tock % 120 == 0 || m_sync_user_presets_now.exchange(false, std::memory_order_acq_rel)) {
tick_tock = 0;
if (m_agent) {
if (!m_agent->is_user_login()) {
@@ -6721,9 +6825,24 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
int total_count = 0;
sync_count = preset_bundle->prints.get_user_presets(preset_bundle, presets_to_sync);
auto sync_with_lock = [this](Preset& preset) {
bool force = false;
{
std::scoped_lock lock(conflict_ids_mutex);
auto it = std::find_if(m_pending_conflict_setting_ids.begin(), m_pending_conflict_setting_ids.end(),
[&preset](const std::string& id) { return id == preset.setting_id; });
if (it != m_pending_conflict_setting_ids.end()) {
force = true;
m_pending_conflict_setting_ids.erase(it);
}
}
sync_preset(&preset, force);
};
if (sync_count > 0) {
for (Preset& preset : presets_to_sync) {
sync_preset(&preset);
sync_with_lock(preset);
boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
}
}
@@ -6732,7 +6851,7 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
sync_count = preset_bundle->filaments.get_user_presets(preset_bundle, presets_to_sync);
if (sync_count > 0) {
for (Preset& preset : presets_to_sync) {
sync_preset(&preset);
sync_with_lock(preset);
boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
}
}
@@ -6741,7 +6860,7 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
sync_count = preset_bundle->printers.get_user_presets(preset_bundle, presets_to_sync);
if (sync_count > 0) {
for (Preset& preset : presets_to_sync) {
sync_preset(&preset);
sync_with_lock(preset);
boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
}
}
@@ -6918,6 +7037,35 @@ void GUI_App::restart_sync_user_preset()
}).detach();
}
void GUI_App::force_push_conflicting_preset(const std::string& setting_id)
{
if (setting_id.empty() || !preset_bundle)
return;
// Queue the id so the next push-sync re-uploads this preset with force=true.
{
std::scoped_lock lock(conflict_ids_mutex);
m_pending_conflict_setting_ids.push_back(setting_id);
}
// The 409 left this preset on "hold", which get_user_presets() skips. Restore it to
// "update" so the next push-sync re-includes it and consumes the queued force flag.
// (We must NOT pull from the cloud here as the Pull path does — that would overwrite
// the local changes the user is trying to force-push.)
PresetCollection* collections[] = {&preset_bundle->prints, &preset_bundle->filaments, &preset_bundle->printers};
for (PresetCollection* coll : collections) {
for (const Preset& preset : coll->get_presets()) {
if (preset.setting_id == setting_id && preset.sync_info == "hold") {
coll->set_sync_info_and_save(preset.name, preset.setting_id, "update", 0);
break;
}
}
}
// Nudge the sync loop to push on its next tick instead of waiting for the 60s cadence.
m_sync_user_presets_now.store(true, std::memory_order_release);
}
void GUI_App::on_stealth_mode_enter()
{
stop_sync_user_preset();
@@ -8543,6 +8691,7 @@ wxString GUI_App::current_language_code_safe() const
{ "pt", "pt_BR", },
{ "lt", "lt_LT", },
{ "vi", "vi_VN", },
{ "th", "th_TH", },
};
wxString language_code = this->current_language_code().BeforeFirst('_');
auto it = mapping.find(language_code);
+7 -1
View File
@@ -297,6 +297,7 @@ private:
NetworkAgent* m_agent { nullptr };
std::map<std::string, std::string> need_delete_presets; // store setting ids of preset
std::vector<bool> m_create_preset_blocked { false, false, false, false, false, false }; // excceed limit
std::vector<std::string> m_pending_conflict_setting_ids; // setting_id from the most recent 409 conflict
bool m_networking_compatible { false };
bool m_networking_need_update { false };
bool m_networking_cancel_update { false };
@@ -322,6 +323,8 @@ private:
boost::thread m_sync_update_thread;
std::shared_ptr<int> m_user_sync_token;
std::atomic<bool> m_restart_sync_pending {false};
std::atomic<bool> m_sync_user_presets_now {false}; // request the sync loop to push user presets on its next tick
std::atomic<bool> m_migration_retry_pending {false};
bool m_is_dark_mode{ false };
bool m_adding_script_handler { false };
bool m_side_popup_status{false};
@@ -529,10 +532,13 @@ public:
void add_pending_vendor_preset(const std::pair<std::string, std::map<std::string, std::string>>& preset_data);
void load_pending_vendors();
void sync_preset(Preset* preset);
void sync_preset(Preset* preset, bool force = false);
void start_sync_user_preset(bool with_progress_dlg = false);
void stop_sync_user_preset();
void restart_sync_user_preset();
// Resolve a cloud sync 409 by force-pushing the conflicting preset: clears the "hold"
// state the conflict left behind and queues it to be re-uploaded with force=true.
void force_push_conflicting_preset(const std::string& setting_id);
void on_stealth_mode_enter();
// Bundle subscription sync
+3
View File
@@ -126,6 +126,9 @@ std::map<std::string, std::vector<SimpleSettingData>> SettingsFactory::PART_CATE
{"lateral_lattice_angle_1", "", 1},
{"lateral_lattice_angle_2", "", 1},
{"infill_overhang_angle", "", 1},
{"lightning_overhang_angle", "", 1},
{"lightning_prune_angle", "", 1},
{"lightning_straightening_angle", "", 1},
{"infill_anchor", "", 1},
{"infill_anchor_max", "", 1},
{"top_surface_pattern", "", 1},
+34
View File
@@ -481,6 +481,25 @@ void ObjectList::create_objects_ctrl()
// Trigger the editor opening manually
this->EditItem(event.GetItem(), GetColumn(colFilament));
#endif
return;
}
// Double-clicking an object/part/instance row frames it in the 3D view,
// matching the "Fit camera to scene or selected object" canvas button.
// The preceding single click has already synced the canvas selection via
// wxEVT_DATAVIEW_SELECTION_CHANGED, so we just trigger the zoom here.
// No-op in slice-preview mode: the camera is shared with the editor
// canvas, so zooming there would move the preview view too — and the
// preview canvas's own toolbar button intentionally resets to the bed.
const wxDataViewItem item = event.GetItem();
if (!item.IsOk())
return;
if (wxGetApp().plater()->is_preview_shown())
return;
const ItemType type = m_objects_model->GetItemType(item);
if (type & (itObject | itVolume | itInstance)) {
if (GLCanvas3D* canvas = wxGetApp().plater()->get_current_canvas3D())
canvas->zoom_to_selection();
}
});
@@ -495,6 +514,16 @@ void ObjectList::create_objects_ctrl()
for (int cn = colName; cn < colCount; cn++)
GetColumn(cn)->SetWidth(m_columns_width[cn] * em);
#endif
// Force an explicit row height on all platforms so the object-list spacing is
// consistent and the filament colour badge (2*em tall, see
// get_extruder_color_icons()) always fits. This is required on macOS, where wx
// 3.3's native wxDataViewCtrl uses a fixed font-line-height row and does NOT
// grow it to fit custom renderers' GetSize() (badges would otherwise overflow
// and merge into adjacent rows); on Windows/Linux the generic control normally
// derives the height from the renderers, but we set it here too so all
// platforms match.
SetRowHeight(2 * em + FromDIP(2));
}
void ObjectList::get_selected_item_indexes(int& obj_idx, int& vol_idx, const wxDataViewItem& input_item/* = wxDataViewItem(nullptr)*/)
@@ -6318,6 +6347,11 @@ void ObjectList::msw_rescale()
for (int cn = colName; cn < colCount; cn++)
GetColumn(cn)->SetWidth(m_columns_width[cn] * em);
// Keep the explicit row height (see create_objects_ctrl) in sync with the
// rescaled em so the filament colour badge keeps fitting after a DPI or theme
// change.
SetRowHeight(2 * em + FromDIP(2));
// rescale/update existing items with bitmaps
m_objects_model->Rescale();
+10 -11
View File
@@ -793,6 +793,8 @@ indexed_triangle_set GLGizmoCut3D::its_make_groove_plane()
indexed_triangle_set mesh;
// handle multiple dovetails/grooves
m_groove_vertices.clear();
m_groove_vertices.reserve(8 * groove_count);
for (int i = 0; i < groove_count; ++i) {
bool is_first_groove = i == 0; // when a groove is not the last groove, then limit the extent of the right plane so that it doesnt overlap the next groove
bool is_last_groove = i == groove_count - 1; // do the same in reverse if a groove is not the first groove
@@ -806,17 +808,14 @@ indexed_triangle_set GLGizmoCut3D::its_make_groove_plane()
// Vertices of the groove used to detection if groove is valid (not used in mesh)
{
m_groove_vertices.clear();
m_groove_vertices.reserve(8);
m_groove_vertices.emplace_back(Vec3f(-slot_neck_outer_x, -plane_half_height, slot_front_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(-slot_mouth_inner_x, plane_half_height, slot_front_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(-slot_mouth_outer_x, -plane_half_height, slot_back_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(-slot_neck_outer_x, plane_half_height, slot_back_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(slot_neck_outer_x, -plane_half_height, slot_front_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(slot_mouth_inner_x, plane_half_height, slot_front_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(slot_mouth_outer_x, -plane_half_height, slot_back_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(slot_neck_outer_x, plane_half_height, slot_back_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(-slot_neck_outer_x + offset_x, -plane_half_height, slot_front_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(-slot_mouth_inner_x + offset_x, plane_half_height, slot_front_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(-slot_mouth_outer_x + offset_x, -plane_half_height, slot_back_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(-slot_neck_outer_x + offset_x, plane_half_height, slot_back_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(slot_neck_outer_x + offset_x, -plane_half_height, slot_front_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(slot_mouth_inner_x + offset_x, plane_half_height, slot_front_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(slot_mouth_outer_x + offset_x, -plane_half_height, slot_back_z).cast<double>());
m_groove_vertices.emplace_back(Vec3f(slot_neck_outer_x + offset_x, plane_half_height, slot_back_z).cast<double>());
}
// ___
+23 -1
View File
@@ -448,7 +448,29 @@ void GLGizmoScale3D::do_scale_uniform(const UpdateData& data)
if (ratio > 0.0)
{
m_scale = m_starting.scale * ratio;
m_offset = Vec3d::Zero();
if (m_starting.ctrl_down && abs(ratio-1.0f)>0.001) {
m_scale.z() = m_starting.scale.z();
double local_offset_x = 0.5 * (m_scale.x() - m_starting.scale.x()) * m_starting.box.size().x();
double local_offset_y = 0.5 * (m_scale.y() - m_starting.scale.y()) * m_starting.box.size().y();
Vec3d local_offset_vec = Vec3d::Zero();
switch (m_hover_id)
{
case 6: { local_offset_vec = Vec3d(-local_offset_x, -local_offset_y, 0.0); break; }
case 7: { local_offset_vec = Vec3d( local_offset_x, -local_offset_y, 0.0); break; }
case 8: { local_offset_vec = Vec3d( local_offset_x, local_offset_y, 0.0); break; }
case 9: { local_offset_vec = Vec3d(-local_offset_x, local_offset_y, 0.0); break; }
default: break;
}
if (m_object_manipulation->is_world_coordinates()) {
m_offset = local_offset_vec;
} else {
m_offset = m_grabbers_tran.get_matrix_no_offset() * local_offset_vec;
}
} else {
m_offset = Vec3d::Zero();
}
}
}
+16 -1
View File
@@ -1386,8 +1386,13 @@ bool GLGizmosManager::activate_gizmo(EType type)
UndoRedo::SnapshotType::LeavingGizmoWithAction);
}
if (type == Undefined) {
if (type == Undefined) {
// it is deactivation of gizmo
if (m_restore_realistic_view_after_paint && wxGetApp().app_config != nullptr) {
wxGetApp().app_config->set_bool(SETTING_OPENGL_REALISTIC_MODE, true);
wxGetApp().app_config->save();
m_restore_realistic_view_after_paint = false;
}
m_current = Undefined;
return true;
}
@@ -1396,6 +1401,16 @@ bool GLGizmosManager::activate_gizmo(EType type)
GLGizmoBase& new_gizmo = *m_gizmos[type];
if (!new_gizmo.is_activable()) return false;
if (type == Seam || type == FdmSupports || type == FuzzySkin) {
if (wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_MODE)) {
m_restore_realistic_view_after_paint = true;
wxGetApp().app_config->set_bool(SETTING_OPENGL_REALISTIC_MODE, false);
wxGetApp().app_config->save();
}
} else {
m_restore_realistic_view_after_paint = false;
}
if (!m_serializing && new_gizmo.wants_enter_leave_snapshots())
Plater::TakeSnapshot snapshot(wxGetApp().plater(),
new_gizmo.get_gizmo_entering_text(),
@@ -150,6 +150,7 @@ private:
static std::map<int, void*> icon_list;
bool m_is_dark = false;
bool m_restore_realistic_view_after_paint = false;
/// <summary>
/// Process mouse event on gizmo toolbar
+14 -5
View File
@@ -4,6 +4,7 @@
#include "DeviceManager.hpp"
#include "DeviceCore/DevManager.h"
#include "DeviceCore/DevUtil.h"
#include "libslic3r/AppConfig.hpp"
#include <boost/log/trivial.hpp>
@@ -13,6 +14,14 @@ static const char* HMS_LOCAL_IMG_PATH = "hms/local_image";
// the local HMS info
static unordered_set<string> package_dev_id_types {"094", "239", "093", "22E"};
// HMS should be disabled when stealth mode is on or networking is not installed
static bool should_disable_hms()
{
Slic3r::AppConfig* config = Slic3r::GUI::wxGetApp().app_config;
if (!config) return true;
return config->get_stealth_mode() || !config->get_bool("installed_networking");
}
namespace Slic3r {
namespace GUI {
@@ -21,7 +30,7 @@ int get_hms_info_version(std::string& version)
AppConfig* config = wxGetApp().app_config;
if (!config)
return -1;
if (config->get_stealth_mode())
if (should_disable_hms())
return -1;
std::string hms_host = config->get_hms_host();
if(hms_host.empty()) {
@@ -61,7 +70,7 @@ int HMSQuery::download_hms_related(const std::string& hms_type, const std::strin
AppConfig* config = wxGetApp().app_config;
if (!config) return -1;
if (config->get_stealth_mode()) return -1;
if (should_disable_hms()) return -1;
std::string hms_host = wxGetApp().app_config->get_hms_host();
std::string lang;
@@ -546,7 +555,7 @@ wxString HMSQuery::query_print_image_action(const MachineObject* obj, int print_
::sprintf(buf, "%08X", print_error);
//The first three digits of SN number
const auto result = _query_error_image_action(get_dev_id_type(obj),std::string(buf), button_action);
if (wxGetApp().app_config->get_stealth_mode() && result.Contains("http")) {
if (should_disable_hms() && result.Contains("http")) {
return wxEmptyString;
}
return result;
@@ -637,7 +646,7 @@ std::string get_hms_wiki_url(std::string error_code)
{
AppConfig* config = wxGetApp().app_config;
if (!config) return "";
if (config->get_stealth_mode()) return "";
if (should_disable_hms()) return "";
std::string hms_host = wxGetApp().app_config->get_hms_host();
std::string lang_code = HMSQuery::hms_language_code();
@@ -663,7 +672,7 @@ std::string get_hms_wiki_url(std::string error_code)
std::string get_error_message(int error_code)
{
if (wxGetApp().app_config->get_stealth_mode()) return "";
if (should_disable_hms()) return "";
char buf[64];
std::string result_str = "";
+262 -138
View File
@@ -2,6 +2,7 @@
#include "libslic3r/GCode.hpp"
#include "GUI_App.hpp"
#include "NotificationManager.hpp"
#include "Widgets/StateColor.hpp"
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
@@ -30,7 +31,6 @@ static const ImU32 GROOVE_COLOR_DARK = IM_COL32(45, 45, 49, 255);
static const ImU32 GROOVE_COLOR_LIGHT = IM_COL32(206, 206, 206, 255);
static const ImU32 BRAND_COLOR = IM_COL32(0, 150, 136, 255);
static int m_tick_value = -1;
static ImVec4 m_tick_rect;
@@ -178,7 +178,6 @@ int IMSlider::GetActiveValue() const
void IMSlider::SetLowerValue(const int lower_val)
{
m_selection = ssLower;
m_lower_value = lower_val;
correct_lower_value();
set_as_dirty();
@@ -186,7 +185,6 @@ void IMSlider::SetLowerValue(const int lower_val)
void IMSlider::SetHigherValue(const int higher_val)
{
m_selection = ssHigher;
m_higher_value = higher_val;
correct_higher_value();
set_as_dirty();
@@ -455,7 +453,7 @@ bool IMSlider::switch_one_layer_mode()
m_is_one_layer = !m_is_one_layer;
if (!m_is_one_layer) { // DEACTIVATE
m_one_layer_value = GetHigherValue(); // ORCA Backup value on deactivate
m_one_layer_value = GetHigherValue(); // ORCA Backup value on deactivate
SetLowerValue(m_min_value);
SetHigherValue(m_max_value); // Higher value resets on toggling off one layer mode to show whole model
}else{ // ACTIVATE
@@ -465,11 +463,10 @@ bool IMSlider::switch_one_layer_mode()
SetHigherValue(m_one_layer_value);
}
else if(GetHigherValue() == m_max_value) // ORCA Prefer backup value if higher value reseted
SetHigherValue(m_one_layer_value); // ORCA Restore value
SetHigherValue(m_one_layer_value); // ORCA Restore value
else // ORCA Prefer higher value if user changed higher value. so it will show section on same view
SetHigherValue(GetHigherValue()); // ORCA use same position with higher value if user changed its position. visible section stays same when switching one layer mode with this
}
m_selection == ssLower ? correct_lower_value() : correct_higher_value();
if (m_selection == ssUndef) m_selection = ssHigher;
set_as_dirty();
return true;
@@ -503,15 +500,23 @@ bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int
const float handle_radius = 12.0f * m_scale;
const float handle_border = 2.0f * m_scale;
const float text_frame_rounding = 2.0f * scale * m_scale;
const float text_start_offset = 8.0f * m_scale;
const ImVec2 text_padding = ImVec2(5.0f, 2.0f) * m_scale;
const float triangle_offsets[3] = {-3.5f * m_scale, 3.5f * m_scale, -6.06f * m_scale};
const ImU32 white_bg = m_is_dark ? BACKGROUND_COLOR_DARK : BACKGROUND_COLOR_LIGHT;
const ImU32 handle_clr = BRAND_COLOR;
const ImU32 handle_border_clr = m_is_dark ? BACKGROUND_COLOR_DARK : BACKGROUND_COLOR_LIGHT;
const wxColour label_bg = StateColor::darkModeColorFor(wxGetApp().get_window_default_clr());
const wxColour label_border = StateColor::darkModeColorFor(wxColour("#CECECE"));
const wxColour rail_inner_bg = m_is_dark ? StateColor::darkModeColorFor(wxColour("#CECECE")) : wxGetApp().get_highlight_default_clr();
const wxColour rail_border = m_is_dark ? StateColor::darkModeColorFor(wxColour("#F0F0F1")) : wxColour("#CECECE");
const ImU32 label_bg_clr = IM_COL32(label_bg.Red(), label_bg.Green(), label_bg.Blue(), 238);
const ImU32 label_border_clr = IM_COL32(label_border.Red(), label_border.Green(), label_border.Blue(), 255);
const ImU32 label_shadow_clr = m_is_dark ? IM_COL32(0, 0, 0, 84) : IM_COL32(0, 0, 0, 38);
ImVec4 range_fill = ImGui::ColorConvertU32ToFloat4(BRAND_COLOR);
range_fill.w = (m_is_dark ? 210.0f : 190.0f) / 255.0f;
const ImU32 range_fill_clr = ImGui::GetColorU32(range_fill);
const ImU32 rail_inner_clr = IM_COL32(rail_inner_bg.Red(), rail_inner_bg.Green(), rail_inner_bg.Blue(), 255);
const ImU32 rail_border_clr = IM_COL32(rail_border.Red(), rail_border.Green(), rail_border.Blue(), 190);
// calculate groove size
const ImVec2 groove_start = ImVec2(pos.x + handle_dummy_width, pos.y + size.y - ONE_LAYER_MARGIN.y * m_scale - (ONE_LAYER_BUTTON_SIZE.y / 2) * m_scale * 0.5f - GROOVE_WIDTH * m_scale * 0.5f);
@@ -521,8 +526,8 @@ bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int
const float mid_y = groove.GetCenter().y;
// set mouse active region. active region.
bool hovered = ImGui::ItemHoverable(draw_region, id);
if (hovered && context.IO.MouseDown[0]) {
bool slider_hovered = ImGui::ItemHoverable(draw_region, id);
if (slider_hovered && context.IO.MouseDown[0]) {
ImGui::SetActiveID(id, window);
ImGui::SetFocusID(id, window);
ImGui::FocusWindow(window);
@@ -530,6 +535,9 @@ bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int
// draw background
draw_background_and_groove(bg_rect, groove);
window->DrawList->AddRect(groove.Min, groove.Max, rail_border_clr, 0.5f * groove.GetHeight(), 0, 1.0f * m_scale);
const ImRect rail_inner(groove.Min + ImVec2(2.0f, 2.0f) * m_scale, groove.Max - ImVec2(2.0f, 2.0f) * m_scale);
window->DrawList->AddRectFilled(rail_inner.Min, rail_inner.Max, rail_inner_clr, 0.5f * rail_inner.GetHeight());
// set scrollable region
const ImRect slideable_region = ImRect(bg_rect.Min + ImVec2(handle_radius, 0.0f), bg_rect.Max - ImVec2(handle_radius, 0.0f));
@@ -543,25 +551,29 @@ bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int
ImVec2 handle_center = handle.GetCenter();
// draw scroll line
ImRect scroll_line = ImRect(groove.Min, ImVec2(handle_center.x, groove.Max.y));
window->DrawList->AddRectFilled(scroll_line.Min, scroll_line.Max, handle_clr, 0.5f * GROOVE_WIDTH * m_scale);
ImRect scroll_line = ImRect(ImVec2(groove.Min.x, groove.Min.y - 2.0f * m_scale),
ImVec2(handle_center.x, groove.Max.y + 2.0f * m_scale));
window->DrawList->AddRectFilled(scroll_line.Min, scroll_line.Max, range_fill_clr, 0.5f * scroll_line.GetHeight());
// draw handle
window->DrawList->AddCircleFilled(handle_center, handle_radius + 2.0f * m_scale, handle_border_clr);
window->DrawList->AddCircleFilled(handle_center, handle_radius, handle_border_clr);
window->DrawList->AddCircleFilled(handle_center, handle_radius - handle_border, handle_clr);
window->DrawList->AddCircle(handle_center, handle_radius + 3.0f * m_scale, handle_clr, 0, 2.0f * m_scale);
// draw label
auto text_utf8 = into_u8(std::to_string(*value));
ImVec2 text_content_size = ImGui::CalcTextSize(text_utf8.c_str());
const std::string value_label = std::to_string(*value);
const ImVec2 text_content_size = ImGui::CalcTextSize(value_label.c_str());
ImVec2 text_size = text_content_size + text_padding * 2;
ImVec2 text_start = ImVec2(handle_center.x + handle_radius + text_start_offset, handle_center.y - 0.5 * text_size.y);
ImRect text_rect(text_start, text_start + text_size);
ImGui::RenderFrame(text_rect.Min, text_rect.Max, white_bg, false, text_frame_rounding);
ImVec2 pos_1 = ImVec2(text_rect.Min.x, text_rect.GetCenter().y + triangle_offsets[0]);
ImVec2 pos_2 = ImVec2(text_rect.Min.x, text_rect.GetCenter().y + triangle_offsets[1]);
ImVec2 pos_3 = ImVec2(text_rect.Min.x + triangle_offsets[2], text_rect.GetCenter().y);
window->DrawList->AddTriangleFilled(pos_1, pos_2, pos_3, white_bg);
ImGui::RenderText(text_start + text_padding, std::to_string(*value).c_str());
const float label_rounding = 5.0f * m_scale;
const ImVec2 shadow_offset = ImVec2(2.0f, 2.0f) * m_scale;
window->DrawList->AddRectFilled(text_rect.Min + shadow_offset, text_rect.Max + shadow_offset, label_shadow_clr, label_rounding);
ImGui::RenderFrame(text_rect.Min, text_rect.Max, label_bg_clr, false, label_rounding);
window->DrawList->AddRect(text_rect.Min, text_rect.Max, label_border_clr, label_rounding, 0, 1.0f * m_scale);
ImGui::RenderText(text_rect.Min + ImVec2((text_size.x - text_content_size.x) * 0.5f,
(text_size.y - text_content_size.y) * 0.5f), value_label.c_str());
return value_changed;
}
@@ -881,18 +893,27 @@ bool IMSlider::vertical_slider(const char* str_id, int* higher_value, int* lower
const float handle_border = 2.0f * m_scale;
const float line_width = 1.0f * m_scale;
const float line_length = 12.0f * m_scale;
const float one_handle_offset = 26.0f * m_scale;
const float bar_width = 28.0f * m_scale;
const float text_frame_rounding = 2.0f * scale * m_scale;
const ImVec2 text_padding = ImVec2(5.0f, 2.0f) * m_scale;
const ImVec2 triangle_offsets[3] = {ImVec2(2.0f, 0.0f) * m_scale, ImVec2(0.0f, 8.0f) * m_scale, ImVec2(9.0f, 0.0f) * m_scale};
ImVec2 text_content_size;
ImVec2 text_size;
const ImU32 white_bg = m_is_dark ? BACKGROUND_COLOR_DARK : BACKGROUND_COLOR_LIGHT;
const ImU32 handle_clr = BRAND_COLOR;
const ImU32 handle_border_clr = m_is_dark ? BACKGROUND_COLOR_DARK : BACKGROUND_COLOR_LIGHT;
const wxColour label_bg = StateColor::darkModeColorFor(wxGetApp().get_window_default_clr());
const wxColour label_bg_active = StateColor::darkModeColorFor(wxColour("#E5F0EE"));
const wxColour label_border = StateColor::darkModeColorFor(wxColour("#CECECE"));
const wxColour rail_inner_bg = m_is_dark ? StateColor::darkModeColorFor(wxColour("#CECECE")) : wxGetApp().get_highlight_default_clr();
const wxColour rail_border = m_is_dark ? StateColor::darkModeColorFor(wxColour("#F0F0F1")) : wxColour("#CECECE");
const ImU32 label_bg_clr = IM_COL32(label_bg.Red(), label_bg.Green(), label_bg.Blue(), 238);
const ImU32 label_bg_active_clr = IM_COL32(label_bg_active.Red(), label_bg_active.Green(), label_bg_active.Blue(), 246);
const ImU32 label_border_clr = IM_COL32(label_border.Red(), label_border.Green(), label_border.Blue(), 255);
const ImU32 label_shadow_clr = m_is_dark ? IM_COL32(0, 0, 0, 84) : IM_COL32(0, 0, 0, 38);
ImVec4 range_fill = ImGui::ColorConvertU32ToFloat4(BRAND_COLOR);
range_fill.w = (m_is_dark ? 210.0f : 190.0f) / 255.0f;
const ImU32 range_fill_clr = ImGui::GetColorU32(range_fill);
const ImU32 rail_inner_clr = IM_COL32(rail_inner_bg.Red(), rail_inner_bg.Green(), rail_inner_bg.Blue(), 255);
const ImU32 rail_border_clr = IM_COL32(rail_border.Red(), rail_border.Green(), rail_border.Blue(), 190);
// calculate slider groove size
const ImVec2 groove_start = ImVec2(pos.x + size.x - ONE_LAYER_MARGIN.x * m_scale - (ONE_LAYER_BUTTON_SIZE.x / 2) * m_scale * 0.5f - GROOVE_WIDTH * m_scale * 0.5f, pos.y + text_dummy_height);
const ImVec2 groove_size = ImVec2(GROOVE_WIDTH * m_scale, size.y - 2 * text_dummy_height);
@@ -900,22 +921,9 @@ bool IMSlider::vertical_slider(const char* str_id, int* higher_value, int* lower
const ImRect bg_rect = ImRect(groove.Min - ImVec2(6.0f, 6.0f) * m_scale, groove.Max + ImVec2(6.0f, 6.0f) * m_scale);
const float mid_x = groove.GetCenter().x;
// ORCA: tune label box width to fit the slider window without overlapping the groove.
const float label_extra_padding = 10.0f * m_scale;
const float one_layer_extra_padding = 6.0f * m_scale;
const float label_width_margin = 10.0f * m_scale;
const float max_label_width = std::max(0.0f,
groove.Min.x - draw_region.Min.x - triangle_offsets[2].x - text_padding.x * 2.0f - label_extra_padding);
// set mouse active region.
const ImRect active_region = ImRect(ImVec2(draw_region.Min.x + 35.0f * m_scale, draw_region.Min.y), draw_region.Max);
bool hovered = ImGui::ItemHoverable(active_region, id) && !ImGui::ItemHoverable(m_tick_rect, id);
if (hovered && context.IO.MouseDown[0]) {
ImGui::SetActiveID(id, window);
ImGui::SetFocusID(id, window);
ImGui::FocusWindow(window);
}
// draw background
draw_background_and_groove(bg_rect, groove);
groove.Min.x - draw_region.Min.x - label_width_margin * 2.0f - text_padding.x * 2.0f);
// Processing interacting
// set scrollable region
@@ -931,34 +939,162 @@ bool IMSlider::vertical_slider(const char* str_id, int* higher_value, int* lower
float lower_handle_pos = get_pos_from_value(v_min, v_max, *lower_value, lower_slideable_region);
ImRect lower_handle = ImRect(mid_x - handle_radius, lower_handle_pos - handle_radius, mid_x + handle_radius, lower_handle_pos + handle_radius);
ImRect one_handle = ImRect(higher_handle.Min - ImVec2(one_handle_offset, 0), higher_handle.Max - ImVec2(one_handle_offset, 0));
auto one_layer_handle = [&](int value) {
const float handle_pos = get_pos_from_value(v_min, v_max, value, one_slideable_region);
return ImRect(mid_x - handle_radius, handle_pos - handle_radius,
mid_x + handle_radius, handle_pos + handle_radius);
};
ImRect one_handle;
if (one_layer_flag)
one_handle = one_layer_handle(*higher_value);
// Label hit testing enables delta-based label drag without jumping to the mouse position.
SelectedSlider hovered_label = ssUndef;
const bool menu_open = ImGui::IsPopupOpen("slider_add_menu_popup") || ImGui::IsPopupOpen("slider_edit_menu_popup");
const ImVec2 higher_text_content_size = ImGui::CalcTextSize(into_u8(higher_label).c_str());
const ImVec2 lower_text_content_size = one_layer_flag ? ImVec2() : ImGui::CalcTextSize(into_u8(lower_label).c_str());
auto label_hit = [&](const ImRect& label_rect, SelectedSlider selection_value) {
if (!label_rect.Contains(context.IO.MousePos))
return;
hovered_label = selection_value;
};
auto range_label_rect = [&](const ImRect& handle, const ImVec2& content_size, bool top_label) {
const ImVec2 text_size = ImVec2(max_label_width, content_size.y) + text_padding * 2.0f;
const ImVec2 text_start = ImVec2(handle.Min.x - text_size.x - label_width_margin,
top_label ? handle.GetCenter().y - text_size.y : handle.GetCenter().y);
return ImRect(text_start, text_start + text_size + ImVec2(label_width_margin, 0.0f));
};
auto one_layer_label_rect = [&](const ImRect& handle) {
const ImVec2 text_size = ImVec2(max_label_width, higher_text_content_size.y) + text_padding * 2.0f;
const ImVec2 text_start = ImVec2(handle.Min.x - text_size.x - label_width_margin,
handle.GetCenter().y - 0.5f * text_size.y);
return ImRect(text_start, text_start + text_size);
};
auto draw_label = [&](const ImRect& rect, const ImVec2& content_size, const std::string& label, bool hovered, bool active) {
const float rounding = 5.0f * m_scale;
const ImU32 bg_clr = active ? label_bg_active_clr : label_bg_clr;
const ImVec2 shadow_offset = ImVec2(2.0f, 2.0f) * m_scale;
window->DrawList->AddRectFilled(rect.Min + shadow_offset, rect.Max + shadow_offset, label_shadow_clr, rounding);
ImGui::RenderFrame(rect.Min, rect.Max, bg_clr, false, rounding);
window->DrawList->AddRect(rect.Min, rect.Max, hovered ? handle_clr : label_border_clr, rounding, 0, hovered ? 1.5f * m_scale : 1.0f * m_scale);
const ImVec2 rect_size = rect.GetSize();
ImGui::RenderText(rect.Min + ImVec2((rect_size.x - content_size.x) * 0.5f,
(rect_size.y - content_size.y) * 0.5f), label.c_str());
};
auto draw_handle = [&](const ImVec2& center) {
window->DrawList->AddCircleFilled(center, handle_radius, handle_border_clr);
window->DrawList->AddCircleFilled(center, handle_radius - handle_border, handle_clr);
};
auto draw_active_handle = [&](const ImVec2& center) {
window->DrawList->AddCircleFilled(center, handle_radius + 2.0f * m_scale, handle_border_clr);
draw_handle(center);
window->DrawList->AddCircle(center, handle_radius + 3.0f * m_scale, handle_clr, 0, 2.0f * m_scale);
window->DrawList->AddLine(center + ImVec2(-0.5f * line_length, 0.0f), center + ImVec2(0.5f * line_length, 0.0f), white_bg, line_width);
window->DrawList->AddLine(center + ImVec2(0.0f, -0.5f * line_length), center + ImVec2(0.0f, 0.5f * line_length), white_bg, line_width);
};
// Prevent interaction with labels if slider add/edit menu is open
// or the mouse was pressed elsewhere and then dragged over them.
if (!menu_open && (!context.IO.MouseDown[0] || context.IO.MouseClicked[0])) {
if (!one_layer_flag) {
label_hit(range_label_rect(higher_handle, higher_text_content_size, true), ssHigher);
label_hit(range_label_rect(lower_handle, lower_text_content_size, false), ssLower);
} else {
label_hit(one_layer_label_rect(one_handle), ssHigher);
}
}
// set mouse active region
const ImRect slider_active_region = ImRect(ImVec2(draw_region.Min.x + 35.0f * m_scale, draw_region.Min.y), draw_region.Max);
bool slider_hovered = !menu_open && ImGui::ItemHoverable(slider_active_region, id) && !ImGui::ItemHoverable(m_tick_rect, id) && hovered_label == ssUndef;
struct LabelDragState
{
ImGuiID id = 0;
SelectedSlider selection = ssUndef;
ImVec2 start_mouse;
int start_value = 0;
};
// Persist the label that started the drag after the cursor leaves its rect.
static LabelDragState label_drag;
if (hovered_label != ssUndef && context.IO.MouseClicked[0]) {
selection = hovered_label;
label_drag.id = id;
label_drag.selection = hovered_label;
label_drag.start_mouse = context.IO.MousePos;
label_drag.start_value = hovered_label == ssHigher ? *higher_value : *lower_value;
ImGui::SetActiveID(id, window);
ImGui::SetFocusID(id, window);
ImGui::FocusWindow(window);
}
if (slider_hovered && context.IO.MouseDown[0]) {
ImGui::SetActiveID(id, window);
ImGui::SetFocusID(id, window);
ImGui::FocusWindow(window);
}
// draw background
draw_background_and_groove(bg_rect, groove);
window->DrawList->AddRect(groove.Min, groove.Max, rail_border_clr, 0.5f * groove.GetWidth(), 0, 1.0f * m_scale);
const ImRect rail_inner(groove.Min + ImVec2(2.0f, 2.0f) * m_scale, groove.Max - ImVec2(2.0f, 2.0f) * m_scale);
window->DrawList->AddRectFilled(rail_inner.Min, rail_inner.Max, rail_inner_clr, 0.5f * rail_inner.GetWidth());
bool value_changed = false;
if (!one_layer_flag)
{
// select higher handle by default
static bool h_selected = (selection == ssHigher);
if (ImGui::ItemHoverable(higher_handle, id) && context.IO.MouseClicked[0]) {
selection = ssHigher;
h_selected = true;
}
if (ImGui::ItemHoverable(lower_handle, id) && context.IO.MouseClicked[0]) {
selection = ssLower;
h_selected = false;
const SelectedSlider dragged_label = label_drag.id == id && context.IO.MouseDown[0] ? label_drag.selection : ssUndef;
if (dragged_label == ssUndef && !menu_open) {
if (ImGui::ItemHoverable(higher_handle, id) && context.IO.MouseClicked[0]) {
selection = ssHigher;
}
if (ImGui::ItemHoverable(lower_handle, id) && context.IO.MouseClicked[0]) {
selection = ssLower;
}
}
bool h_selected = selection != ssLower;
// update handle position and value
if (h_selected)
{
value_changed = slider_behavior(id, higher_slideable_region, v_min, v_max,
higher_value, &higher_handle, ImGuiSliderFlags_Vertical,
m_tick_value, m_tick_rect);
}
if (!h_selected) {
value_changed = slider_behavior(id, lower_slideable_region, v_min, v_max,
lower_value, &lower_handle, ImGuiSliderFlags_Vertical,
m_tick_value, m_tick_rect);
if (dragged_label != ssUndef) {
const ImRect& drag_region = dragged_label == ssHigher ? higher_slideable_region : lower_slideable_region;
const float region_height = drag_region.GetHeight();
if (region_height > 0.0f) {
const float delta = context.IO.MousePos.y - label_drag.start_mouse.y;
const float value_delta = delta * (float)(v_max - v_min) / region_height;
const int new_value = (int)ImClamp((float)label_drag.start_value - value_delta, (float)v_min, (float)v_max);
if (dragged_label == ssHigher) {
value_changed = *higher_value != new_value;
*higher_value = new_value;
} else {
value_changed = *lower_value != new_value;
*lower_value = new_value;
}
}
h_selected = dragged_label == ssHigher;
if (dragged_label == ssHigher) {
higher_handle_pos = get_pos_from_value(v_min, v_max, *higher_value, higher_slideable_region);
higher_handle = ImRect(mid_x - handle_radius, higher_handle_pos - handle_radius, mid_x + handle_radius, higher_handle_pos + handle_radius);
} else {
lower_handle_pos = get_pos_from_value(v_min, v_max, *lower_value, lower_slideable_region);
lower_handle = ImRect(mid_x - handle_radius, lower_handle_pos - handle_radius, mid_x + handle_radius, lower_handle_pos + handle_radius);
}
} else {
if (h_selected)
{
value_changed = slider_behavior(id, higher_slideable_region, v_min, v_max,
higher_value, &higher_handle, ImGuiSliderFlags_Vertical,
m_tick_value, m_tick_rect);
}
if (!h_selected) {
value_changed = slider_behavior(id, lower_slideable_region, v_min, v_max,
lower_value, &lower_handle, ImGuiSliderFlags_Vertical,
m_tick_value, m_tick_rect);
}
}
SelectedSlider active_label = ssUndef;
if (dragged_label != ssUndef)
active_label = dragged_label;
else if (context.ActiveId == id && context.IO.MouseDown[0])
active_label = h_selected ? ssHigher : ssLower;
ImVec2 higher_handle_center = higher_handle.GetCenter();
ImVec2 lower_handle_center = lower_handle.GetCenter();
@@ -978,10 +1114,10 @@ bool IMSlider::vertical_slider(const char* str_id, int* higher_value, int* lower
}
// judge whether to open menu
if (ImGui::ItemHoverable(h_selected ? higher_handle : lower_handle, id) && context.IO.MouseClicked[1])
if (!menu_open && ImGui::ItemHoverable(h_selected ? higher_handle : lower_handle, id) && context.IO.MouseClicked[1])
m_show_menu = true;
if ((!ImGui::ItemHoverable(h_selected ? higher_handle : lower_handle, id) && context.IO.MouseClicked[1]) ||
context.IO.MouseClicked[0])
if (!menu_open && ((!ImGui::ItemHoverable(h_selected ? higher_handle : lower_handle, id) && context.IO.MouseClicked[1]) ||
context.IO.MouseClicked[0]))
m_show_menu = false;
// draw ticks
@@ -991,107 +1127,94 @@ bool IMSlider::vertical_slider(const char* str_id, int* higher_value, int* lower
if (!m_ticks.has_tick_with_code(ToolChange)) {
// draw scroll line
ImRect scroll_line = ImRect(ImVec2(groove.Min.x, higher_handle_center.y), ImVec2(groove.Max.x, lower_handle_center.y));
window->DrawList->AddRectFilled(scroll_line.Min, scroll_line.Max, handle_clr);
ImRect scroll_line = ImRect(ImVec2(groove.Min.x - 2.0f * m_scale, higher_handle_center.y),
ImVec2(groove.Max.x + 2.0f * m_scale, lower_handle_center.y));
window->DrawList->AddRectFilled(scroll_line.Min, scroll_line.Max, range_fill_clr, 0.5f * scroll_line.GetWidth());
}
// draw handles
window->DrawList->AddCircleFilled(higher_handle_center, handle_radius, handle_border_clr);
window->DrawList->AddCircleFilled(higher_handle_center, handle_radius - handle_border, handle_clr);
window->DrawList->AddCircleFilled(lower_handle_center, handle_radius, handle_border_clr);
window->DrawList->AddCircleFilled(lower_handle_center, handle_radius - handle_border, handle_clr);
if (h_selected) {
window->DrawList->AddCircleFilled(higher_handle_center, handle_radius, handle_border_clr);
window->DrawList->AddCircleFilled(higher_handle_center, handle_radius - handle_border, handle_clr);
window->DrawList->AddLine(higher_handle_center + ImVec2(-0.5f * line_length, 0.0f), higher_handle_center + ImVec2(0.5f * line_length, 0.0f), white_bg, line_width);
window->DrawList->AddLine(higher_handle_center + ImVec2(0.0f, -0.5f * line_length), higher_handle_center + ImVec2(0.0f, 0.5f * line_length), white_bg, line_width);
}
if (!h_selected) {
window->DrawList->AddLine(lower_handle_center + ImVec2(-0.5f * line_length, 0.0f), lower_handle_center + ImVec2(0.5f * line_length, 0.0f), white_bg, line_width);
window->DrawList->AddLine(lower_handle_center + ImVec2(0.0f, -0.5f * line_length), lower_handle_center + ImVec2(0.0f, 0.5f * line_length), white_bg, line_width);
}
draw_handle(higher_handle_center);
draw_handle(lower_handle_center);
draw_active_handle(h_selected ? higher_handle_center : lower_handle_center);
// ORCA: render fixed-width label boxes
// draw higher label
auto text_utf8 = into_u8(higher_label);
text_content_size = ImGui::CalcTextSize(text_utf8.c_str());
text_size = ImVec2(max_label_width, text_content_size.y) + text_padding * 2;
ImVec2 text_start = ImVec2(higher_handle.Min.x - text_size.x - triangle_offsets[2].x, higher_handle_center.y - text_size.y);
ImRect text_rect(text_start, text_start + text_size);
ImGui::RenderFrame(text_rect.Min, text_rect.Max, white_bg, false, text_frame_rounding);
ImVec2 pos_1 = text_rect.Max - triangle_offsets[0];
ImVec2 pos_2 = pos_1 - triangle_offsets[1];
ImVec2 pos_3 = pos_1 + triangle_offsets[2];
window->DrawList->AddTriangleFilled(pos_1, pos_2, pos_3, white_bg);
ImGui::RenderText(text_start + ImVec2((text_size.x - text_content_size.x) * 0.5f,
(text_size.y - text_content_size.y) * 0.5f), higher_label.c_str());
// draw lower label
text_utf8 = into_u8(lower_label);
text_content_size = ImGui::CalcTextSize(text_utf8.c_str());
text_size = ImVec2(max_label_width, text_content_size.y) + text_padding * 2;
text_start = ImVec2(lower_handle.Min.x - text_size.x - triangle_offsets[2].x, lower_handle_center.y);
text_rect = ImRect(text_start, text_start + text_size);
ImGui::RenderFrame(text_rect.Min, text_rect.Max, white_bg, false, text_frame_rounding);
pos_1 = ImVec2(text_rect.Max.x, text_rect.Min.y) - triangle_offsets[0];
pos_2 = pos_1 + triangle_offsets[1];
pos_3 = pos_1 + triangle_offsets[2];
window->DrawList->AddTriangleFilled(pos_1, pos_2, pos_3, white_bg);
ImGui::RenderText(text_start + ImVec2((text_size.x - text_content_size.x) * 0.5f,
(text_size.y - text_content_size.y) * 0.5f), lower_label.c_str());
// ORCA: render fixed-width label boxes
// draw higher label
text_size = ImVec2(max_label_width, higher_text_content_size.y) + text_padding * 2;
ImVec2 text_start = ImVec2(higher_handle.Min.x - text_size.x - label_width_margin, higher_handle_center.y - text_size.y);
ImRect text_rect(text_start, text_start + text_size);
const bool higher_label_active = active_label == ssHigher;
draw_label(text_rect, higher_text_content_size, higher_label,
hovered_label == ssHigher || higher_label_active, higher_label_active);
// draw lower label
text_size = ImVec2(max_label_width, lower_text_content_size.y) + text_padding * 2;
text_start = ImVec2(lower_handle.Min.x - text_size.x - label_width_margin, lower_handle_center.y);
text_rect = ImRect(text_start, text_start + text_size);
const bool lower_label_active = active_label == ssLower;
draw_label(text_rect, lower_text_content_size, lower_label,
hovered_label == ssLower || lower_label_active, lower_label_active);
// draw mouse position
if (hovered) {
if (slider_hovered && !context.IO.MouseDown[0]) {
draw_tick_on_mouse_position(h_selected ? higher_slideable_region : lower_slideable_region);
}
}
if (one_layer_flag)
{
// update handle position
value_changed = slider_behavior(id, one_slideable_region, v_min, v_max,
higher_value, &one_handle, ImGuiSliderFlags_Vertical,
m_tick_value, m_tick_rect);
const SelectedSlider dragged_label = label_drag.id == id && context.IO.MouseDown[0] ? label_drag.selection : ssUndef;
if (dragged_label == ssHigher) {
const float region_height = one_slideable_region.GetHeight();
if (region_height > 0.0f) {
const float delta = context.IO.MousePos.y - label_drag.start_mouse.y;
const float value_delta = delta * (float)(v_max - v_min) / region_height;
const int new_value = (int)ImClamp((float)label_drag.start_value - value_delta, (float)v_min, (float)v_max);
value_changed = *higher_value != new_value;
*higher_value = new_value;
}
one_handle = one_layer_handle(*higher_value);
} else {
value_changed = slider_behavior(id, one_slideable_region, v_min, v_max,
higher_value, &one_handle, ImGuiSliderFlags_Vertical,
m_tick_value, m_tick_rect);
}
ImVec2 handle_center = one_handle.GetCenter();
// judge whether to open menu
if (ImGui::ItemHoverable(one_handle, id) && context.IO.MouseClicked[1])
if (!menu_open && ImGui::ItemHoverable(one_handle, id) && context.IO.MouseClicked[1])
m_show_menu = true;
if ((!ImGui::ItemHoverable(one_handle, id) && context.IO.MouseClicked[1]) ||
context.IO.MouseClicked[0])
if (!menu_open && ((!ImGui::ItemHoverable(one_handle, id) && context.IO.MouseClicked[1]) ||
context.IO.MouseClicked[0]))
m_show_menu = false;
ImVec2 bar_center = higher_handle.GetCenter();
// draw ticks
draw_ticks(one_slideable_region);
// draw colored band
draw_colored_band(groove, one_slideable_region);
// draw handle
window->DrawList->AddLine(ImVec2(mid_x - 0.5 * bar_width, handle_center.y), ImVec2(mid_x + 0.5 * bar_width, handle_center.y), handle_clr, 2 * line_width);
window->DrawList->AddCircleFilled(handle_center, handle_radius, handle_border_clr);
window->DrawList->AddCircleFilled(handle_center, handle_radius - handle_border, handle_clr);
window->DrawList->AddLine(handle_center + ImVec2(-0.5f * line_length, 0.0f), handle_center + ImVec2(0.5f * line_length, 0.0f), white_bg, line_width);
window->DrawList->AddLine(handle_center + ImVec2(0.0f, -0.5f * line_length), handle_center + ImVec2(0.0f, 0.5f * line_length), white_bg, line_width);
draw_active_handle(handle_center);
// draw label
auto text_utf8 = into_u8(higher_label);
text_content_size = ImGui::CalcTextSize(text_utf8.c_str());
// ORCA: slightly narrower label box in one-layer mode to avoid left shift.
text_size = ImVec2(std::max(0.0f, max_label_width - label_extra_padding - one_layer_extra_padding),
text_content_size.y) + text_padding * 2;
ImVec2 text_start = ImVec2(one_handle.Min.x - text_size.x, handle_center.y - 0.5 * text_size.y);
text_size = ImVec2(max_label_width, higher_text_content_size.y) + text_padding * 2;
ImVec2 text_start = ImVec2(one_handle.Min.x - text_size.x - label_width_margin, handle_center.y - 0.5 * text_size.y);
ImRect text_rect = ImRect(text_start, text_start + text_size);
ImGui::RenderFrame(text_rect.Min, text_rect.Max, white_bg, false, text_frame_rounding);
ImGui::RenderText(text_start + ImVec2((text_size.x - text_content_size.x) * 0.5f,
(text_size.y - text_content_size.y) * 0.5f), higher_label.c_str());
const bool label_active = context.ActiveId == id && context.IO.MouseDown[0];
draw_label(text_rect, higher_text_content_size, higher_label, hovered_label == ssHigher || label_active, label_active);
// draw mouse position
if (hovered) {
if (slider_hovered && !context.IO.MouseDown[0]) {
draw_tick_on_mouse_position(one_slideable_region);
}
}
if (!context.IO.MouseDown[0] && label_drag.id == id) {
label_drag.id = 0;
label_drag.selection = ssUndef;
if (context.ActiveId == id)
ImGui::ClearActiveID();
}
return value_changed;
}
@@ -1130,8 +1253,6 @@ bool IMSlider::render(int canvas_width, int canvas_height)
imgui.set_next_window_pos(canvas_width, 0.5f * static_cast<float>(canvas_height), ImGuiCond_Always, 1.0f, 0.5f);
imgui.begin(std::string("laysers_slider"), windows_flag);
render_menu();
int higher_value = GetHigherValue();
int lower_value = GetLowerValue();
std::string higher_label = get_label(m_higher_value);
@@ -1146,6 +1267,7 @@ bool IMSlider::render(int canvas_width, int canvas_height)
SetLowerValue(lower_value);
result = true;
}
render_menu();
imgui.end();
imgui.set_next_window_pos(canvas_width, canvas_height, ImGuiCond_Always, 1.0f, 1.0f);
@@ -1362,8 +1484,10 @@ void IMSlider::render_add_menu()
{
int extruder_num = m_extruder_colors.size();
if (m_show_menu)
if (m_show_menu) {
ImGui::OpenPopup("slider_add_menu_popup");
m_show_menu = false;
}
if (ImGui::BeginPopup("slider_add_menu_popup")) {
bool menu_item_enable = m_draw_mode != dmSequentialFffPrint;
bool hovered = false;
@@ -1415,8 +1539,10 @@ void IMSlider::render_add_menu()
void IMSlider::render_edit_menu(const TickCode& tick)
{
if (m_show_menu)
if (m_show_menu) {
ImGui::OpenPopup("slider_edit_menu_popup");
m_show_menu = false;
}
if (ImGui::BeginPopup("slider_edit_menu_popup")) {
switch (tick.type)
{
@@ -1700,5 +1826,3 @@ std::array<int, 2> IMSlider::get_active_extruders_for_tick(int tick) const
}
} // Slic3r
+8
View File
@@ -2789,6 +2789,9 @@ void ImGuiWrapper::init_font(bool compress)
if(m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesKorean()) {
font_name_regular = "NanumGothic-Regular.ttf";
font_name_bold = "NanumGothic-Bold.ttf";
} else if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) {
font_name_regular = "Sarabun-Medium.ttf";
font_name_bold = "Sarabun-SemiBold.ttf";
}
default_font = io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/" + font_name_regular).c_str(), m_font_size, &cfg, ranges.Data);
if (default_font == nullptr) {
@@ -2804,6 +2807,11 @@ void ImGuiWrapper::init_font(bool compress)
if (bold_font == nullptr) { throw Slic3r::RuntimeError("ImGui: Could not load deafult font"); }
}
if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) {
default_font->Scale *= 1.25f;
bold_font->Scale *= 1.25f;
}
#ifdef _WIN32
// Render the text a bit larger (see GLCanvas3D::_resize() and issue #3401), but only if the scale factor
// for the Display is greater than 300%.
@@ -705,7 +705,7 @@ static void convert_object_to_vertices(const Slic3r::PrintObject& object, const
continue;
const Slic3r::PrintRegionConfig& cfg = layerm->region().config();
if (has_perimeters) {
const size_t extruder_id = static_cast<size_t>(std::max(cfg.wall_filament.value - 1, 0));
const size_t extruder_id = static_cast<size_t>(std::max(cfg.outer_wall_filament_id.value - 1, 0));
convert_to_vertices(layerm->perimeters, layer_z, layer_id, extruder_id,
object_helper.color_id(layer_z, extruder_id), EGCodeExtrusionRole::ExternalPerimeter,
copy, data.vertices);
@@ -715,10 +715,13 @@ static void convert_object_to_vertices(const Slic3r::PrintObject& object, const
// fill represents infill extrusions of a single island.
const auto& fill = *dynamic_cast<const Slic3r::ExtrusionEntityCollection*>(ee);
if (!fill.entities.empty()) {
const bool is_solid_infill = Slic3r::is_solid_infill(fill.entities.front()->role());
const Slic3r::ExtrusionRole role = fill.entities.front()->role();
const bool is_solid_infill = Slic3r::is_solid_infill(role);
const size_t extruder_id = is_solid_infill ?
static_cast<size_t>(std::max(cfg.solid_infill_filament.value - 1, 0)) :
static_cast<size_t>(std::max(cfg.sparse_infill_filament.value - 1, 0));
static_cast<size_t>(std::max((role == Slic3r::erTopSolidInfill || role == Slic3r::erIroning ? cfg.top_surface_filament_id.value :
role == Slic3r::erBottomSurface ? cfg.bottom_surface_filament_id.value :
cfg.internal_solid_filament_id.value) - 1, 0)) :
static_cast<size_t>(std::max(cfg.sparse_infill_filament_id.value - 1, 0));
convert_to_vertices(fill, layer_z, layer_id, extruder_id,
object_helper.color_id(layer_z, extruder_id),
is_solid_infill ? EGCodeExtrusionRole::SolidInfill : EGCodeExtrusionRole::InternalInfill,
+86 -31
View File
@@ -791,6 +791,42 @@ void NotificationManager::PopNotification::render_hypertext(ImGuiWrapper& imgui,
}
void NotificationManager::PopNotification::render_hyperlink_action(ImGuiWrapper& imgui, float text_x, float text_y,
const std::string& text, const char* button_id, const std::function<void()>& on_click)
{
// Invisible button over the label
ImVec2 part_size = ImGui::CalcTextSize(text.c_str());
ImGui::SetCursorPosX(text_x - 4);
ImGui::SetCursorPosY(text_y - 5);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f));
if (imgui.button(button_id, part_size.x + 6, part_size.y + 10) && on_click)
on_click();
ImGui::PopStyleColor(3);
// Hover color
ImVec4 color = m_HyperTextColor;
if (ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly))
color = m_HyperTextColorHover;
// Text
push_style_color(ImGuiCol_Text, color, m_state == EState::FadingOut, m_current_fade_opacity);
ImGui::SetCursorPosX(text_x);
ImGui::SetCursorPosY(text_y);
imgui.text(text.c_str());
ImGui::PopStyleColor();
// Underline
ImVec2 lineEnd = ImGui::GetItemRectMax();
lineEnd.y -= 2;
ImVec2 lineStart = lineEnd;
lineStart.x = ImGui::GetItemRectMin().x;
ImGui::GetWindowDrawList()->AddLine(lineStart, lineEnd,
IM_COL32((int)(color.x * 255), (int)(color.y * 255), (int)(color.z * 255),
(int)(color.w * 255.f * (m_state == EState::FadingOut ? m_current_fade_opacity : 1.f))));
}
void NotificationManager::PopNotification::render_close_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
{
ensure_ui_inited();
@@ -2346,40 +2382,49 @@ bool NotificationManager::SharedProfilesNotification::on_text_click()
void NotificationManager::SharedProfilesNotification::render_hypertext(ImGuiWrapper& imgui,
const float text_x, const float text_y, const std::string text, bool more)
{
// Invisible button
ImVec2 part_size = ImGui::CalcTextSize(text.c_str());
ImGui::SetCursorPosX(text_x - 4);
ImGui::SetCursorPosY(text_y - 5);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f));
if (imgui.button("##browse_btn", part_size.x + 6, part_size.y + 10)) {
if (on_text_click()) {
close();
render_hyperlink_action(imgui, text_x, text_y, text, "##browse_btn",
[this] { if (on_text_click()) close(); });
}
void NotificationManager::OrcaSyncConflictNotification::init()
{
PopNotification::init();
// Reserve a dedicated action row for the two conflict-resolution links.
m_lines_count = m_lines_count + 1;
}
void NotificationManager::OrcaSyncConflictNotification::render_text(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y)
{
float x_offset = m_left_indentation;
float shift_y = m_line_height;
float starting_y = m_line_height / 2;
int last_end = 0;
std::string line;
for (size_t i = 0; i < m_endlines.size(); i++) {
if (m_text1.size() >= m_endlines[i]) {
line = m_text1.substr(last_end, m_endlines[i] - last_end);
last_end = m_endlines[i];
if (m_text1.size() > m_endlines[i])
last_end += (m_text1[m_endlines[i]] == '\n' || m_text1[m_endlines[i]] == ' ' ? 1 : 0);
ImGui::SetCursorPosX(x_offset);
ImGui::SetCursorPosY(starting_y + i * shift_y);
imgui.text(line.c_str());
}
}
ImGui::PopStyleColor(3);
// Hover color
ImVec4 HyperColor = m_HyperTextColor;
if (ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly))
HyperColor = m_HyperTextColorHover;
// Text
push_style_color(ImGuiCol_Text, HyperColor, m_state == EState::FadingOut, m_current_fade_opacity);
ImGui::SetCursorPosX(text_x);
ImGui::SetCursorPosY(text_y);
imgui.text(text.c_str());
ImGui::PopStyleColor();
// Underline
ImVec2 lineEnd = ImGui::GetItemRectMax();
lineEnd.y -= 2;
ImVec2 lineStart = lineEnd;
lineStart.x = ImGui::GetItemRectMin().x;
ImGui::GetWindowDrawList()->AddLine(lineStart, lineEnd,
IM_COL32((int)(HyperColor.x * 255), (int)(HyperColor.y * 255), (int)(HyperColor.z * 255),
(int)(HyperColor.w * 255.f * (m_state == EState::FadingOut ? m_current_fade_opacity : 1.f))));
const float action_y = starting_y + m_endlines.size() * shift_y;
const std::string pull_text = _u8L("Pull");
render_hyperlink_action(imgui, x_offset, action_y, pull_text, "##orca_sync_pull",
[this] { if (m_pull_callback && m_pull_callback(m_evt_handler)) close(); });
if (m_force_push_callback) {
const std::string force_push_text = _u8L("Force push");
const float force_x = x_offset + ImGui::CalcTextSize((pull_text + " ").c_str()).x;
render_hyperlink_action(imgui, force_x, action_y, force_push_text, "##orca_sync_force_push",
[this] { if (m_force_push_callback && m_force_push_callback(m_evt_handler)) close(); });
}
}
void NotificationManager::push_shared_profiles_notification(const std::string& explore_url)
@@ -2391,6 +2436,16 @@ void NotificationManager::push_shared_profiles_notification(const std::string& e
push_notification_data(std::make_unique<NotificationManager::SharedProfilesNotification>(data, m_id_provider, m_evt_handler, explore_url), 0);
}
void NotificationManager::push_orca_sync_conflict_notification(const std::string& text,
std::function<bool(wxEvtHandler*)> pull_callback,
std::function<bool(wxEvtHandler*)> force_push_callback)
{
close_notification_of_type(NotificationType::OrcaSyncConflict);
NotificationData data{ NotificationType::OrcaSyncConflict, NotificationLevel::WarningNotificationLevel, 0, text };
push_notification_data(std::make_unique<NotificationManager::OrcaSyncConflictNotification>(
data, m_id_provider, m_evt_handler, std::move(pull_callback), std::move(force_push_callback)), 0);
}
void NotificationManager::push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback)
{
// If already exists
+34
View File
@@ -15,6 +15,8 @@
#include <wx/time.h>
#include <string>
#include <functional>
#include <utility>
#include <vector>
#include <deque>
#include <unordered_set>
@@ -162,6 +164,8 @@ enum class NotificationType
BBLMixUsePLAAndPETG,
BBLNozzleFilamentIncompatible,
OrcaSharedProfilesAvailable,
OrcaCloudAPIError,
OrcaSyncConflict,
NotificationTypeCount
};
@@ -274,6 +278,9 @@ public:
// Shared profiles available for selected printer
void push_shared_profiles_notification(const std::string& explore_url);
void push_orca_sync_conflict_notification(const std::string& text,
std::function<bool(wxEvtHandler*)> pull_callback,
std::function<bool(wxEvtHandler*)> force_push_callback);
// Download URL progress notif
void push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback);
@@ -491,6 +498,11 @@ private:
const float text_x, const float text_y,
const std::string text,
bool more = false);
// Renders an underlined, hyperlink-style clickable label backed by an invisible button.
// on_click runs when pressed; the callback itself decides whether to close().
void render_hyperlink_action(ImGuiWrapper& imgui, float text_x, float text_y,
const std::string& text, const char* button_id,
const std::function<void()>& on_click);
virtual void bbl_render_block_notif_text(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y);
@@ -887,6 +899,28 @@ private:
std::string m_explore_url;
bool m_dont_show_clicked{ false };
};
class OrcaSyncConflictNotification : public PopNotification
{
public:
OrcaSyncConflictNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler,
std::function<bool(wxEvtHandler*)> pull_callback,
std::function<bool(wxEvtHandler*)> force_push_callback)
: PopNotification(n, id_provider, evt_handler)
, m_pull_callback(std::move(pull_callback))
, m_force_push_callback(std::move(force_push_callback))
{
m_multiline = true;
}
protected:
void init() override;
void render_text(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y) override;
std::function<bool(wxEvtHandler*)> m_pull_callback;
std::function<bool(wxEvtHandler*)> m_force_push_callback;
};
class SlicingProgressNotification;
// in HintNotification.hpp
+24 -14
View File
@@ -229,7 +229,7 @@ ObjColorPanel::ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, c
specify_color_cluster_title->SetFont(Label::Head_14);
specify_cluster_sizer->Add(specify_color_cluster_title, 0, wxALIGN_CENTER | wxALL, FromDIP(5));
m_color_cluster_num_by_user_ebox = new SpinInput(m_page_simple, "", wxEmptyString, wxDefaultPosition, wxSize(FromDIP(45), -1), wxTE_PROCESS_ENTER);
m_color_cluster_num_by_user_ebox = new SpinInput(m_page_simple, "", wxEmptyString, wxDefaultPosition, wxSize(FromDIP(60), -1), wxTE_PROCESS_ENTER);
m_color_cluster_num_by_user_ebox->SetValue(std::to_string(m_color_cluster_num_by_algo).c_str());
m_color_cluster_num_by_user_ebox->SetToolTip(_L("Enter or click the adjustment button to modify number again"));
{//event
@@ -284,11 +284,8 @@ ObjColorPanel::ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, c
}
}
wxStaticText *combox_title = new wxStaticText(m_page_simple, wxID_ANY, _L("view"), wxPoint(FromDIP(216), FromDIP(312)));
// combox_title->SetTransparent(true);
combox_title->SetBackgroundColour(wxColour(240, 240, 240, 0));
combox_title->SetForegroundColour(wxColour(107, 107, 107, 100));
auto cur_combox = new ComboBox(m_page_simple, wxID_ANY, wxEmptyString, wxPoint(FromDIP(250), FromDIP(310)), wxSize(FromDIP(100), -1), 0, NULL, wxCB_READONLY);
wxStaticText *combox_title = new wxStaticText(m_page_simple, wxID_ANY, _L("view"));
auto cur_combox = new ComboBox(m_page_simple, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(100), -1), 0, NULL, wxCB_READONLY);
wxArrayString choices = get_all_camera_view_type();
for (size_t i = 0; i < choices.size(); i++) { cur_combox->Append(choices[i]); }
cur_combox->SetSelection(0);
@@ -310,11 +307,18 @@ ObjColorPanel::ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, c
wxBORDER_NONE | wxBU_AUTODRAW);
m_image_button->SetBitmap(image);
m_image_button->SetCanFocus(false);
#ifdef __WXGTK__
RemoveButtonBorder(m_image_button);
#endif
icon_sizer->Add(m_image_button, 0, wxEXPAND | wxALL,
FromDIP(0)); // wxEXPAND | wxALL
cur_combox->Raise();//for mac
m_sizer_simple->Add(icon_sizer, FromDIP(0), wxALIGN_CENTER | wxALL, FromDIP(0));
auto view_sizer = new wxBoxSizer(wxHORIZONTAL);
view_sizer->Add(combox_title, 0, wxALIGN_CENTER | wxALL, FromDIP(5));
view_sizer->Add(cur_combox , 0, wxALIGN_CENTER | wxALL, FromDIP(5));
m_sizer_simple->Add(view_sizer, 0, wxALIGN_RIGHT | wxRIGHT, FromDIP(20));
}
wxBoxSizer * current_filaments_title_sizer = new wxBoxSizer(wxHORIZONTAL);
wxStaticText *current_filaments_title = new wxStaticText(m_page_simple, wxID_ANY, _L("Current filament colors"));
@@ -357,7 +361,7 @@ ObjColorPanel::ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, c
m_scrolledWindow->ShowScrollbars(wxScrollbarVisibility::wxSHOW_SB_NEVER, wxScrollbarVisibility::wxSHOW_SB_DEFAULT);
draw_new_table();
m_sizer_simple->Add(m_scrolledWindow, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5));
m_sizer_simple->Add(m_scrolledWindow, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(15));
//buttons
wxBoxSizer *quick_set_sizer = new wxBoxSizer(wxHORIZONTAL);
quick_set_sizer->AddSpacer(FromDIP(25));
@@ -522,9 +526,12 @@ wxBoxSizer *ObjColorPanel::create_reset_btn_sizer(wxWindow *parent)
wxBoxSizer *ObjColorPanel::create_extruder_icon_and_rgba_sizer(wxWindow *parent, int id, const wxColour &color)
{
auto icon_sizer = new wxBoxSizer(wxHORIZONTAL);
wxButton *icon = new wxButton(parent, wxID_ANY, {}, wxDefaultPosition, ICON_SIZE, wxBORDER_NONE | wxBU_AUTODRAW);
icon->SetBitmap(*get_extruder_color_icon(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(), std::to_string(id + 1), FromDIP(16), FromDIP(16)));
wxButton *icon = new wxButton(parent, wxID_ANY, {}, wxDefaultPosition, FromDIP(wxSize(20,20)), wxBORDER_NONE | wxBU_AUTODRAW);
icon->SetBitmap(*get_extruder_color_icon(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(), std::to_string(id + 1), FromDIP(20), FromDIP(20)));
icon->SetCanFocus(false);
#ifdef __WXGTK__
RemoveButtonBorder(icon);
#endif
m_extruder_icon_list.emplace_back(icon);
icon_sizer->Add(icon, 0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, 0); // wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM
//icon_sizer->AddSpacer(FromDIP(5));
@@ -553,10 +560,10 @@ ComboBox *ObjColorPanel::CreateEditorCtrl(wxWindow *parent, int id) // wxRect la
if (icons.empty())
return nullptr;
::ComboBox *c_editor = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(m_combox_width), -1), 0, nullptr,
::ComboBox *c_editor = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, nullptr,
wxCB_READONLY | CB_NO_DROP_ICON | CB_NO_TEXT);
c_editor->SetMinSize(wxSize(FromDIP(m_combox_width), -1));
c_editor->SetMaxSize(wxSize(FromDIP(m_combox_width), -1));
c_editor->SetMinSize(wxSize(icon_width + FromDIP(8), -1)); // match size with bitmap
c_editor->SetMaxSize(wxSize(icon_width + FromDIP(8), -1)); // match size with bitmap
c_editor->GetDropDown().SetUseContentWidth(false);
for (size_t i = 0; i < icons.size(); i++) {
c_editor->Append(wxString::Format("%d", i), *icons[i]);
@@ -910,9 +917,12 @@ wxBoxSizer *ObjColorPanel::create_color_icon_map_rgba_sizer(wxWindow *parent, in
{
auto icon_sizer = new wxBoxSizer(wxHORIZONTAL);
//icon_sizer->AddSpacer(FromDIP(40));
wxButton *icon = new wxButton(parent, wxID_ANY, {}, wxDefaultPosition, ICON_SIZE, wxBORDER_NONE | wxBU_AUTODRAW);
icon->SetBitmap(*get_extruder_color_icon(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(), "", FromDIP(16), FromDIP(16)));
wxButton *icon = new wxButton(parent, wxID_ANY, {}, wxDefaultPosition, FromDIP(wxSize(20,20)), wxBORDER_NONE | wxBU_AUTODRAW);
icon->SetBitmap(*get_extruder_color_icon(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(), "", FromDIP(20), FromDIP(20)));
icon->SetCanFocus(false);
#ifdef __WXGTK__
RemoveButtonBorder(icon);
#endif
m_color_cluster_icon_list.emplace_back(icon);
icon_sizer->Add(icon, 0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, 0); // wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM
icon_sizer->AddSpacer(FromDIP(10));
+129 -47
View File
@@ -1517,9 +1517,16 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
const DynamicPrintConfig& glb_config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
int glb_support_intf_extr = glb_config.opt_int("support_interface_filament");
int glb_support_extr = glb_config.opt_int("support_filament");
int glb_wall_extr = glb_config.opt_int("wall_filament");
int glb_sparse_infill_extr = glb_config.opt_int("sparse_infill_filament");
int glb_solid_infill_extr = glb_config.opt_int("solid_infill_filament");
int glb_outer_wall_extr = glb_config.opt_int("outer_wall_filament_id");
int glb_inner_wall_extr = glb_config.opt_int("inner_wall_filament_id");
if (glb_outer_wall_extr == 0) glb_outer_wall_extr = glb_inner_wall_extr;
if (glb_inner_wall_extr == 0) glb_inner_wall_extr = glb_outer_wall_extr;
int glb_sparse_infill_extr = glb_config.opt_int("sparse_infill_filament_id");
int glb_internal_solid_extr = glb_config.opt_int("internal_solid_filament_id");
int glb_top_surface_extr = glb_config.opt_int("top_surface_filament_id");
int glb_bottom_surface_extr = glb_config.opt_int("bottom_surface_filament_id");
if (glb_top_surface_extr == 0) glb_top_surface_extr = glb_internal_solid_extr;
if (glb_bottom_surface_extr == 0) glb_bottom_surface_extr = glb_internal_solid_extr;
bool glb_support = glb_config.opt_bool("enable_support");
glb_support |= glb_config.opt_int("raft_layers") > 0;
@@ -1573,32 +1580,64 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
plate_extruders.push_back(glb_support_extr);
}
int obj_wall_extr = 1;
const ConfigOption* wall_opt = mo->config.option("wall_filament");
if (wall_opt != nullptr)
obj_wall_extr = wall_opt->getInt();
if (obj_wall_extr != 1)
plate_extruders.push_back(obj_wall_extr);
else if (glb_wall_extr != 1)
plate_extruders.push_back(glb_wall_extr);
int obj_outer_wall_extr = 0;
if (const ConfigOption* wall_opt = mo->config.option("outer_wall_filament_id"); wall_opt != nullptr)
obj_outer_wall_extr = wall_opt->getInt();
if (obj_outer_wall_extr == 0)
if (const ConfigOption* wall_opt = mo->config.option("inner_wall_filament_id"); wall_opt != nullptr)
obj_outer_wall_extr = wall_opt->getInt();
if (obj_outer_wall_extr != 0)
plate_extruders.push_back(obj_outer_wall_extr);
else if (glb_outer_wall_extr != 0)
plate_extruders.push_back(glb_outer_wall_extr);
int obj_sparse_infill_extr = 1;
const ConfigOption* sparse_infill_opt = mo->config.option("sparse_infill_filament");
int obj_inner_wall_extr = 0;
if (const ConfigOption* wall_opt = mo->config.option("inner_wall_filament_id"); wall_opt != nullptr)
obj_inner_wall_extr = wall_opt->getInt();
if (obj_inner_wall_extr == 0)
if (const ConfigOption* wall_opt = mo->config.option("outer_wall_filament_id"); wall_opt != nullptr)
obj_inner_wall_extr = wall_opt->getInt();
if (obj_inner_wall_extr != 0)
plate_extruders.push_back(obj_inner_wall_extr);
else if (glb_inner_wall_extr != 0)
plate_extruders.push_back(glb_inner_wall_extr);
int obj_sparse_infill_extr = 0;
const ConfigOption* sparse_infill_opt = mo->config.option("sparse_infill_filament_id");
if (sparse_infill_opt != nullptr)
obj_sparse_infill_extr = sparse_infill_opt->getInt();
if (obj_sparse_infill_extr != 1)
if (obj_sparse_infill_extr != 0)
plate_extruders.push_back(obj_sparse_infill_extr);
else if (glb_sparse_infill_extr != 1)
else if (glb_sparse_infill_extr != 0)
plate_extruders.push_back(glb_sparse_infill_extr);
int obj_solid_infill_extr = 1;
const ConfigOption* solid_infill_opt = mo->config.option("solid_infill_filament");
if (solid_infill_opt != nullptr)
obj_solid_infill_extr = solid_infill_opt->getInt();
if (obj_solid_infill_extr != 1)
plate_extruders.push_back(obj_solid_infill_extr);
else if (glb_solid_infill_extr != 1)
plate_extruders.push_back(glb_solid_infill_extr);
int obj_internal_solid_extr = 0;
if (const ConfigOption* solid_opt = mo->config.option("internal_solid_filament_id"); solid_opt != nullptr)
obj_internal_solid_extr = solid_opt->getInt();
if (obj_internal_solid_extr != 0)
plate_extruders.push_back(obj_internal_solid_extr);
else if (glb_internal_solid_extr != 0)
plate_extruders.push_back(glb_internal_solid_extr);
int obj_top_surface_extr = 0;
if (const ConfigOption* top_opt = mo->config.option("top_surface_filament_id"); top_opt != nullptr)
obj_top_surface_extr = top_opt->getInt();
if (obj_top_surface_extr == 0)
obj_top_surface_extr = obj_internal_solid_extr;
if (obj_top_surface_extr != 0)
plate_extruders.push_back(obj_top_surface_extr);
else if (glb_top_surface_extr != 0)
plate_extruders.push_back(glb_top_surface_extr);
int obj_bottom_surface_extr = 0;
if (const ConfigOption* bottom_opt = mo->config.option("bottom_surface_filament_id"); bottom_opt != nullptr)
obj_bottom_surface_extr = bottom_opt->getInt();
if (obj_bottom_surface_extr == 0)
obj_bottom_surface_extr = obj_internal_solid_extr;
if (obj_bottom_surface_extr != 0)
plate_extruders.push_back(obj_bottom_surface_extr);
else if (glb_bottom_surface_extr != 0)
plate_extruders.push_back(glb_bottom_surface_extr);
}
@@ -1629,9 +1668,16 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D
// if 3mf file
int glb_support_intf_extr = full_config.opt_int("support_interface_filament");
int glb_support_extr = full_config.opt_int("support_filament");
int glb_wall_extr = full_config.opt_int("wall_filament");
int glb_sparse_infill_extr = full_config.opt_int("sparse_infill_filament");
int glb_solid_infill_extr = full_config.opt_int("solid_infill_filament");
int glb_outer_wall_extr = full_config.opt_int("outer_wall_filament_id");
int glb_inner_wall_extr = full_config.opt_int("inner_wall_filament_id");
if (glb_outer_wall_extr == 0) glb_outer_wall_extr = glb_inner_wall_extr;
if (glb_inner_wall_extr == 0) glb_inner_wall_extr = glb_outer_wall_extr;
int glb_sparse_infill_extr = full_config.opt_int("sparse_infill_filament_id");
int glb_internal_solid_extr = full_config.opt_int("internal_solid_filament_id");
int glb_top_surface_extr = full_config.opt_int("top_surface_filament_id");
int glb_bottom_surface_extr = full_config.opt_int("bottom_surface_filament_id");
if (glb_top_surface_extr == 0) glb_top_surface_extr = glb_internal_solid_extr;
if (glb_bottom_surface_extr == 0) glb_bottom_surface_extr = glb_internal_solid_extr;
bool glb_support = full_config.opt_bool("enable_support");
glb_support |= full_config.opt_int("raft_layers") > 0;
@@ -1695,32 +1741,64 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D
else if (glb_support_extr != 0)
plate_extruders.push_back(glb_support_extr);
int obj_wall_extr = 1;
const ConfigOption* wall_opt = object->config.option("wall_filament");
if (wall_opt != nullptr)
obj_wall_extr = wall_opt->getInt();
if (obj_wall_extr != 1)
plate_extruders.push_back(obj_wall_extr);
else if (glb_wall_extr != 1)
plate_extruders.push_back(glb_wall_extr);
int obj_outer_wall_extr = 0;
if (const ConfigOption* wall_opt = object->config.option("outer_wall_filament_id"); wall_opt != nullptr)
obj_outer_wall_extr = wall_opt->getInt();
if (obj_outer_wall_extr == 0)
if (const ConfigOption* wall_opt = object->config.option("inner_wall_filament_id"); wall_opt != nullptr)
obj_outer_wall_extr = wall_opt->getInt();
if (obj_outer_wall_extr != 0)
plate_extruders.push_back(obj_outer_wall_extr);
else if (glb_outer_wall_extr != 0)
plate_extruders.push_back(glb_outer_wall_extr);
int obj_sparse_infill_extr = 1;
const ConfigOption* sparse_infill_opt = object->config.option("sparse_infill_filament");
int obj_inner_wall_extr = 0;
if (const ConfigOption* wall_opt = object->config.option("inner_wall_filament_id"); wall_opt != nullptr)
obj_inner_wall_extr = wall_opt->getInt();
if (obj_inner_wall_extr == 0)
if (const ConfigOption* wall_opt = object->config.option("outer_wall_filament_id"); wall_opt != nullptr)
obj_inner_wall_extr = wall_opt->getInt();
if (obj_inner_wall_extr != 0)
plate_extruders.push_back(obj_inner_wall_extr);
else if (glb_inner_wall_extr != 0)
plate_extruders.push_back(glb_inner_wall_extr);
int obj_sparse_infill_extr = 0;
const ConfigOption* sparse_infill_opt = object->config.option("sparse_infill_filament_id");
if (sparse_infill_opt != nullptr)
obj_sparse_infill_extr = sparse_infill_opt->getInt();
if (obj_sparse_infill_extr != 1)
if (obj_sparse_infill_extr != 0)
plate_extruders.push_back(obj_sparse_infill_extr);
else if (glb_sparse_infill_extr != 1)
else if (glb_sparse_infill_extr != 0)
plate_extruders.push_back(glb_sparse_infill_extr);
int obj_solid_infill_extr = 1;
const ConfigOption* solid_infill_opt = object->config.option("solid_infill_filament");
if (solid_infill_opt != nullptr)
obj_solid_infill_extr = solid_infill_opt->getInt();
if (obj_solid_infill_extr != 1)
plate_extruders.push_back(obj_solid_infill_extr);
else if (glb_solid_infill_extr != 1)
plate_extruders.push_back(glb_solid_infill_extr);
int obj_internal_solid_extr = 0;
if (const ConfigOption* solid_opt = object->config.option("internal_solid_filament_id"); solid_opt != nullptr)
obj_internal_solid_extr = solid_opt->getInt();
if (obj_internal_solid_extr != 0)
plate_extruders.push_back(obj_internal_solid_extr);
else if (glb_internal_solid_extr != 0)
plate_extruders.push_back(glb_internal_solid_extr);
int obj_top_surface_extr = 0;
if (const ConfigOption* top_opt = object->config.option("top_surface_filament_id"); top_opt != nullptr)
obj_top_surface_extr = top_opt->getInt();
if (obj_top_surface_extr == 0)
obj_top_surface_extr = obj_internal_solid_extr;
if (obj_top_surface_extr != 0)
plate_extruders.push_back(obj_top_surface_extr);
else if (glb_top_surface_extr != 0)
plate_extruders.push_back(glb_top_surface_extr);
int obj_bottom_surface_extr = 0;
if (const ConfigOption* bottom_opt = object->config.option("bottom_surface_filament_id"); bottom_opt != nullptr)
obj_bottom_surface_extr = bottom_opt->getInt();
if (obj_bottom_surface_extr == 0)
obj_bottom_surface_extr = obj_internal_solid_extr;
if (obj_bottom_surface_extr != 0)
plate_extruders.push_back(obj_bottom_surface_extr);
else if (glb_bottom_surface_extr != 0)
plate_extruders.push_back(glb_bottom_surface_extr);
}
}
@@ -3762,7 +3840,11 @@ void PartPlate::on_filament_deleted(int filament_count, int filament_id)
{
if (m_config.has("filament_map")) {
std::vector<int>& filament_maps = m_config.option<ConfigOptionInts>("filament_map")->values;
filament_maps.erase(filament_maps.begin() + filament_id);
// Guard against an out-of-range index: the per-plate filament_map can be out of sync
// with the global filament count, and erasing at/past end() triggers an out-of-bounds
// memmove (crash on macOS, see PartPlate::on_filament_deleted in crash reports).
if (filament_id >= 0 && filament_id < (int) filament_maps.size())
filament_maps.erase(filament_maps.begin() + filament_id);
}
update_first_layer_print_sequence_when_delete_filament(filament_id);
}
+20 -3
View File
@@ -41,6 +41,7 @@
#include "MsgDialog.hpp"
#include "OAuthDialog.hpp"
#include "SimplyPrint.hpp"
#include "3DPrinterOS.hpp"
namespace Slic3r {
namespace GUI {
@@ -292,6 +293,12 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
} else {
msg = r.error_message;
}
} else if (const auto h = dynamic_cast<C3DPrinterOS*>(host.get()); h) {
GUI::MessageDialog dlg(this, _L("Valid session not detected. Proceed with login to 3DPrinterOS?"), _L("Proceed"),
wxICON_INFORMATION | wxYES | wxNO);
if (dlg.ShowModal() == wxID_YES) {
result = h->login(msg);
}
} else {
PrinterCloudAuthDialog dlg(this->GetParent(), host.get());
dlg.ShowModal();
@@ -663,7 +670,8 @@ void PhysicalPrinterDialog::update(bool printer_change)
const auto current_host = temp->GetValue();
if (current_host == L"https://connect.prusa3d.com" ||
current_host == L"https://app.obico.io" ||
current_host == "https://simplyprint.io" || current_host == "https://simplyprint.io/panel") {
current_host == "https://simplyprint.io" || current_host == "https://simplyprint.io/panel" ||
current_host == C3DPrinterOS::default_host()) {
temp->SetValue(wxString());
m_config->opt_string("print_host") = "";
}
@@ -696,7 +704,7 @@ void PhysicalPrinterDialog::update(bool printer_change)
m_config->opt_string("print_host") = "https://app.obico.io";
}
}
} else if (opt->value == htSimplyPrint) {
} else if (opt->value == htSimplyPrint) {
// Set the host url
if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) {
printhost_field->disable();
@@ -733,7 +741,16 @@ void PhysicalPrinterDialog::update(bool printer_change)
m_optgroup->disable_field("printhost_ssl_ignore_revoke");
if (m_printhost_cafile_browse_btn)
m_printhost_cafile_browse_btn->Disable();
}
} else if (opt->value == ht3DPrinterOS) {
if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) {
if (wxTextCtrl* temp = dynamic_cast<TextCtrl*>(printhost_field)->text_ctrl(); temp && temp->GetValue().IsEmpty()) {
temp->SetValue(C3DPrinterOS::default_host());
m_config->opt_string("print_host") = C3DPrinterOS::default_host();
}
}
m_optgroup->hide_field("print_host_webui");
m_optgroup->hide_field("printhost_apikey");
}
}
if (opt->value == htFlashforge) {
+21 -56
View File
@@ -877,54 +877,6 @@ struct DynamicFilamentList : DynamicList
}
};
struct DynamicFilamentList1Based : DynamicFilamentList
{
void apply_on(Choice *c) override
{
if (items.empty())
update(true);
auto cb = dynamic_cast<ComboBox *>(c->window);
auto n = cb->GetSelection();
cb->Clear();
for (auto i : items) {
cb->Append(i.first, *i.second);
}
if (n < cb->GetCount())
cb->SetSelection(n);
}
wxString get_value(int index) override
{
wxString str;
str << index+1;
return str;
}
int index_of(wxString value) override
{
long n = 0;
if(!value.ToLong(&n))
return -1;
--n;
return (n >= 0 && n <= items.size()) ? int(n) : -1;
}
void update(bool force = false)
{
items.clear();
if (!force && m_choices.empty())
return;
auto icons = get_extruder_color_icons(true);
auto presets = wxGetApp().preset_bundle->filament_presets;
for (int i = 0; i < presets.size(); ++i) {
wxString str;
std::string type;
wxGetApp().preset_bundle->filaments.find_preset(presets[i])->get_filament_type(type);
str << type;
items.push_back({str, i < icons.size() ? icons[i] : nullptr});
}
DynamicList::update();
}
};
// Check if the machine supports Junction Deviation (Marlin firmware with machine_max_junction_deviation > 0)
static bool has_junction_deviation(const DynamicPrintConfig* printer_config)
{
@@ -941,7 +893,6 @@ static bool has_junction_deviation(const DynamicPrintConfig* printer_config)
}
static DynamicFilamentList dynamic_filament_list;
static DynamicFilamentList1Based dynamic_filament_list_1_based;
class AMSCountPopupWindow : public PopupWindow
{
@@ -1648,9 +1599,12 @@ Sidebar::Sidebar(Plater *parent)
{
Choice::register_dynamic_list("support_filament", &dynamic_filament_list);
Choice::register_dynamic_list("support_interface_filament", &dynamic_filament_list);
Choice::register_dynamic_list("wall_filament", &dynamic_filament_list_1_based);
Choice::register_dynamic_list("sparse_infill_filament", &dynamic_filament_list_1_based);
Choice::register_dynamic_list("solid_infill_filament", &dynamic_filament_list_1_based);
Choice::register_dynamic_list("outer_wall_filament_id", &dynamic_filament_list);
Choice::register_dynamic_list("inner_wall_filament_id", &dynamic_filament_list);
Choice::register_dynamic_list("sparse_infill_filament_id", &dynamic_filament_list);
Choice::register_dynamic_list("internal_solid_filament_id", &dynamic_filament_list);
Choice::register_dynamic_list("top_surface_filament_id", &dynamic_filament_list);
Choice::register_dynamic_list("bottom_surface_filament_id", &dynamic_filament_list);
Choice::register_dynamic_list("wipe_tower_filament", &dynamic_filament_list);
p->scrolled = new wxPanel(this);
@@ -3769,7 +3723,6 @@ void Sidebar::show_SEMM_buttons()
void Sidebar::update_dynamic_filament_list()
{
dynamic_filament_list.update();
dynamic_filament_list_1_based.update();
}
PlaterPresetComboBox* Sidebar::printer_combox()
@@ -4923,7 +4876,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
"extruder_colour", "filament_colour", "filament_type", "material_colour", "printable_height", "extruder_printable_height", "printer_model", "printer_technology",
// These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor.
"layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height",
"wall_loops", "wall_filament", "sparse_infill_density", "sparse_infill_filament", "top_shell_layers",
"wall_loops", "outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_density", "sparse_infill_filament_id", "top_shell_layers",
"enable_support", "support_filament", "support_interface_filament",
"support_top_z_distance", "support_bottom_z_distance", "raft_layers",
"wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "wipe_tower_max_purge_speed",
@@ -6205,6 +6158,10 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
}
// ORCA: legacy feature-filament default migration (1 -> 0) is now handled
// uniformly in PrintConfigDef::handle_legacy() via the old->new key rename
// (wall_filament -> wall_filament_id, etc.), which also covers saved presets.
// plate data
if (plate_data.size() > 0) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", import 3mf UPDATE_GCODE_RESULT \n");
@@ -9594,6 +9551,12 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
wxGetApp().get_tab(preset_type)->select_preset(preset_name);
}
// ORCA: Always refresh the selected filament combo so its color swatch (clr_picker)
// matches the chosen preset. update_ams_color() (in OnSelect) updates the project
// filament color when the preset defines one; this repaints the swatch to match.
if (preset_type == Preset::TYPE_FILAMENT)
combo->update();
// update plater with new config
q->on_config_change(wxGetApp().preset_bundle->full_config());
if (preset_type == Preset::TYPE_PRINTER) {
@@ -16709,8 +16672,10 @@ void Plater::on_config_change(const DynamicPrintConfig &config)
update_scheduled = true;
}
// Orca: update when *_filament changed
else if (opt_key == "support_interface_filament" || opt_key == "support_filament" || opt_key == "wall_filament" ||
opt_key == "sparse_infill_filament" || opt_key == "solid_infill_filament") {
else if (opt_key == "support_interface_filament" || opt_key == "support_filament" ||
opt_key == "outer_wall_filament_id" || opt_key == "inner_wall_filament_id" ||
opt_key == "sparse_infill_filament_id" || opt_key == "internal_solid_filament_id" ||
opt_key == "top_surface_filament_id" || opt_key == "bottom_surface_filament_id") {
update_scheduled = true;
}
}
+49 -5
View File
@@ -152,7 +152,8 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS
wxLANGUAGE_CATALAN,
wxLANGUAGE_PORTUGUESE_BRAZILIAN,
wxLANGUAGE_LITHUANIAN,
wxLANGUAGE_VIETNAMESE
wxLANGUAGE_VIETNAMESE,
wxLANGUAGE_THAI
};
auto translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY);
@@ -259,6 +260,9 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS
else if (vlist[i] == wxLocale::GetLanguageInfo(wxLANGUAGE_VIETNAMESE)) {
language_name = wxString::FromUTF8("Tiếng Việt");
}
else if (vlist[i] == wxLocale::GetLanguageInfo(wxLANGUAGE_THAI)) {
language_name = wxString::FromUTF8("\xE0\xB9\x84\xE0\xB8\x97\xE0\xB8\xA2");
}
if (app_config->get(param) == vlist[i]->CanonicalName) {
m_current_language_selected = i;
@@ -968,6 +972,11 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too
if (m_sync_user_preset_checkbox) m_sync_user_preset_checkbox->Enable(!enabled);
if (m_bambu_cloud_checkbox) m_bambu_cloud_checkbox->Enable(!enabled);
}
else if (param == "hide_login_side_panel") {
if (wxGetApp().mainframe && wxGetApp().mainframe->m_webview) {
wxGetApp().mainframe->m_webview->SendCloudProvidersInfo();
}
}
#ifdef __WXMSW__
if (param == "associate_3mf") {
@@ -1031,6 +1040,10 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too
}
}
if (param == "show_unsupported_presets") {
wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT);
}
if (param == "enable_high_low_temp_mixed_printing") {
if (checkbox->GetValue()) {
const wxString warning_title = _L("Bed Temperature Difference Warning");
@@ -1172,7 +1185,6 @@ wxBoxSizer* PreferencesDialog::create_item_link_association( wxString url_prefix
auto checkbox = new ::CheckBox(m_parent);
checkbox->SetToolTip(tooltip);
checkbox->SetValue(reg_to_current_instance); // If registered to the current instance, checkbox should be checked
checkbox->Enable(!reg_to_current_instance); // Since unregistering isn't supported, checkbox is disabled when checked
// build text next to checkbox
auto checkbox_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE);
@@ -1223,8 +1235,10 @@ wxBoxSizer* PreferencesDialog::create_item_link_association( wxString url_prefix
v_sizer->Add(registered_instance_title, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(DESIGN_LEFT_MARGIN));
checkbox->Bind(wxEVT_TOGGLEBUTTON, [=](wxCommandEvent& e) {
wxGetApp().associate_url(url_prefix.ToStdWstring());
checkbox->Disable();
if (checkbox->GetValue())
wxGetApp().associate_url(url_prefix.ToStdWstring());
else
wxGetApp().disassociate_url(url_prefix.ToStdWstring());
update_current_association_str();
e.Skip();
});
@@ -1540,6 +1554,30 @@ void PreferencesDialog::create_items()
g_sizer = f_sizers.back();
g_sizer->AddGrowableCol(0, 1);
//// GRAPHICS > Realistic view
g_sizer->Add(create_item_title(_L("Realistic View")), 1, wxEXPAND);
auto item_realistic_phong = create_item_checkbox(
_L("Phong shading"),
_L("Uses Phong shading inside realistic view.")
, SETTING_OPENGL_REALISTIC_PHONG
);
g_sizer->Add(item_realistic_phong);
auto item_realistic_ssao = create_item_checkbox(
_L("SSAO ambient occlusion"),
_L("Applies SSAO in realistic view."),
SETTING_OPENGL_PHONG_SSAO
);
g_sizer->Add(item_realistic_ssao);
auto item_realistic_shadows = create_item_checkbox(
_L("Shadows"),
_L("Renders cast shadows on the plate in realistic view."),
SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS
);
g_sizer->Add(item_realistic_shadows);
//// GRAPHICS > Anti-aliasing
g_sizer->Add(create_item_title(_L("Anti-aliasing")), 1, wxEXPAND);
@@ -1604,9 +1642,12 @@ void PreferencesDialog::create_items()
auto item_region = create_item_region_combobox(_L("Login region"), "");
g_sizer->Add(item_region);
auto item_stealth_mode = create_item_checkbox(_L("Stealth mode"), _L("This disables all cloud services e.g. Orca Cloud and Bambu Cloud. This stops the transmission of data to Bambu's cloud services too. Users who don't use BBL machines or use LAN mode only can safely turn on this function."), "stealth_mode");
auto item_stealth_mode = create_item_checkbox(_L("Stealth mode"), _L("This disables all cloud features, including Orca Cloud profile syncing. Users who prefer to work entirely offline can enable this option.\nNote: When Stealth Mode is enabled, your user profiles will not be backed up to Orca Cloud."), "stealth_mode");
g_sizer->Add(item_stealth_mode);
auto item_hide_login_side_panel = create_item_checkbox(_L("Hide login side panel"), _L("Hide the login side panel on the home page."), "hide_login_side_panel");
g_sizer->Add(item_hide_login_side_panel);
auto item_network_test = create_item_button(_L("Network test"), _L("Test") + " " + dots, "", _L("Open Network Test"), []() {
NetworkTestDialog dlg(wxGetApp().mainframe);
dlg.ShowModal();
@@ -1861,6 +1902,9 @@ void PreferencesDialog::create_items()
auto item_keep_painting = create_item_checkbox(_L("(Experimental) Keep painted feature after mesh change"), _L("Attempt to keep painted features (color/seam/support/fuzzy etc.) after changing the object mesh (such as cut/reload from disk/simplify/fix etc.)\nHighly experimental! Slow and may create artifact."), "keep_painting");
g_sizer->Add(item_keep_painting);
auto item_show_unsupported = create_item_checkbox(_L("Show unsupported presets"), _L("Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."), "show_unsupported_presets");
g_sizer->Add(item_show_unsupported);
g_sizer->Add(create_item_title(_L("Storage")), 1, wxEXPAND);
auto item_allow_abnormal_storage = create_item_checkbox(_L("Allow Abnormal Storage"), _L("This allows the use of Storage that is marked as abnormal by the Printer.\nUse at your own risk, can cause issues!"), "allow_abnormal_storage");
g_sizer->Add(item_allow_abnormal_storage);
+22 -3
View File
@@ -234,7 +234,19 @@ int PresetComboBox::update_ams_color()
std::string ctype;
std::vector<std::string> colors;
if (idx < 0) {
auto name = Preset::remove_suffix_modified(GetValue().ToUTF8().data());
// ORCA: The combo displays the preset alias while
// the stored preset name usually carries a printer suffix. Resolving with the raw display
// value via find_preset() fails for such presets, so this returned early and the
// filament color swatch (clr_picker) kept showing the previous color. Prefer the
// internal preset name stored per item, then fall back to alias resolution.
std::string name;
if (m_last_selected >= 0) {
wxString stored = GetItemAlias(m_last_selected);
if (!stored.empty())
name = Preset::remove_suffix_modified(stored.ToUTF8().data());
}
if (name.empty())
name = m_collection->get_preset_name_by_alias(Preset::remove_suffix_modified(GetValue().ToUTF8().data()));
auto *preset = m_collection->find_preset(name);
if (preset)
color = preset->config.opt_string("default_filament_colour", 0u);
@@ -1402,13 +1414,20 @@ void PlaterPresetComboBox::update()
: group_filament_presets == "2" ? "by_type" // Create sub menus with filament type
: group_filament_presets == "3" ? "by_vendor" // Create sub menus with filament vendor
: ""; // Use without sub menu
add_presets(nonsys_presets, selected_user_preset, L("User presets"), group_filament_presets_by);
// ORCA: the by_type/by_vendor grouping is derived from filament-only attributes
// (filament_type/filament_vendor), which are empty for printer and material presets.
// Applying it to non-filament combos buckets every user preset under "Unspecified",
// so only group user presets by those attributes for the filament combobox.
add_presets(nonsys_presets, selected_user_preset, L("User presets"),
m_type == Preset::TYPE_FILAMENT ? group_filament_presets_by : wxString(""));
// ORCA: add bundle presets with sub-dropdown grouping for filament and printer
auto bundle_group_name = (m_type == Preset::TYPE_FILAMENT || m_type == Preset::TYPE_PRINTER) ? "by_bundle" : "";
add_presets(bundle_presets, selected_bundle_preset, L("Bundle presets"), bundle_group_name);
// BBS: move system to the end
add_presets(system_presets, selected_system_preset, L("System presets"), _L("System"));
add_presets(uncompatible_presets, {}, L("Unsupported presets"), _L("Unsupported") + " ");
// Orca: optionally show unsupported presets (controlled by developer preference, default off)
if (wxGetApp().app_config->get_bool("show_unsupported_presets"))
add_presets(uncompatible_presets, {}, L("Unsupported presets"), _L("Unsupported") + " ");
//BBS: remove unused pysical printer logic
/*if (m_type == Preset::TYPE_PRINTER)
+6 -3
View File
@@ -128,9 +128,12 @@ std::string PresetHints::maximum_volumetric_flow_description(const PresetBundle
auto feature_extruder_active = [idx_extruder, num_extruders](int i) {
return i <= 0 || i > num_extruders || idx_extruder == -1 || idx_extruder == i - 1;
};
bool perimeter_extruder_active = feature_extruder_active(print_config.opt_int("wall_filament"));
bool infill_extruder_active = feature_extruder_active(print_config.opt_int("sparse_infill_filament"));
bool solid_infill_extruder_active = feature_extruder_active(print_config.opt_int("solid_infill_filament"));
bool perimeter_extruder_active = feature_extruder_active(print_config.opt_int("outer_wall_filament_id"))
&& feature_extruder_active(print_config.opt_int("inner_wall_filament_id"));
bool infill_extruder_active = feature_extruder_active(print_config.opt_int("sparse_infill_filament_id"));
bool solid_infill_extruder_active = feature_extruder_active(print_config.opt_int("internal_solid_filament_id"))
&& feature_extruder_active(print_config.opt_int("top_surface_filament_id"))
&& feature_extruder_active(print_config.opt_int("bottom_surface_filament_id"));
bool support_material_extruder_active = feature_extruder_active(print_config.opt_int("support_filament"));
bool support_material_interface_extruder_active = feature_extruder_active(print_config.opt_int("support_interface_filament"));
+1 -3
View File
@@ -243,12 +243,10 @@ void PrinterWebView::SendAPIKey()
m_apikey);
m_browser->RemoveAllUserScripts();
#ifdef _WIN32
// RemoveAllUserScripts causes WebView2 to forget about our script message handler,
// RemoveAllUserScripts causes WebView to forget about our script message handler,
// so re-add it here.
m_browser->RemoveScriptMessageHandler("wx");
m_browser->AddScriptMessageHandler("wx");
#endif
#ifdef __linux__
// Re-inject the vue-resize/WebKitGTK workaround that RemoveAllUserScripts just cleared.
+9 -25
View File
@@ -98,8 +98,6 @@ public:
stop_upload = true;
if (upload_thread.joinable())
upload_thread.join();
if (sn_thread.joinable())
sn_thread.join();
}
void on_script_message(wxWebViewEvent &evt) override
@@ -287,35 +285,21 @@ private:
void handle_get_sn_request(const std::string& request_id, const std::string& method)
{
if (sn_request_in_progress.exchange(true)) {
send_ipc_message("response", request_id, method, 1, "SN request already in progress");
return;
// Panel always calls get_sn with a 10s IPC timeout. Answer immediately from
// dev_sn / cache — do not spawn a thread or perform HTTP (panel uses URL sn on miss).
std::string sn;
if (DynamicPrintConfig* config = get_active_printer_config()) {
const std::unique_ptr<PrintHost> host(PrintHost::get_print_host(config));
if (host)
sn = host->get_sn();
}
if (sn_thread.joinable())
sn_thread.join();
sn_thread = std::thread([this, request_id, method]() {
std::string sn;
DynamicPrintConfig* config = get_active_printer_config();
std::unique_ptr<PrintHost> print_host(config == nullptr ? nullptr : PrintHost::get_print_host(config));
if (print_host != nullptr)
sn = print_host->get_sn();
sn_request_in_progress = false;
json data = {
{"sn", sn}
};
send_ipc_message("response", request_id, method, 0, "success", dump_json(data));
});
json data = { { "sn", sn } };
send_ipc_message("response", request_id, method, 0, "success", dump_json(data));
}
std::atomic<bool> upload_in_progress { false };
std::atomic<bool> sn_request_in_progress { false };
std::atomic<bool> stop_upload { false };
std::thread upload_thread;
std::thread sn_thread;
};
} // namespace
+54 -16
View File
@@ -1935,6 +1935,26 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
}
}
if (opt_key == "parallel_printheads_count" || opt_key == "parallel_printheads_bed_exclude_areas") {
if (m_config->opt_bool("support_parallel_printheads")) {
const int count = opt_key == "parallel_printheads_count" ? boost::any_cast<int>(value) : m_config->opt_int("parallel_printheads_count");
if (auto *field = this->get_field("bed_exclude_area")) {
wxString exclude_area;
if (count > 0) {
if (const auto *areas = m_config->option<ConfigOptionStrings>("parallel_printheads_bed_exclude_areas");
areas != nullptr) {
const size_t index = static_cast<size_t>(count - 1);
if (index < areas->values.size())
exclude_area = wxString::FromUTF8(areas->values[index]);
}
}
field->set_value(exclude_area, true);
field->propagate_value();
}
}
}
if (m_postpone_update_ui) {
// It means that not all values are rolled to the system/last saved values jet.
// And call of the update() can causes a redundant check of the config values,
@@ -2301,13 +2321,14 @@ void TabPrint::build()
optgroup = page->new_optgroup(L("Line width"), L"param_line_width");
optgroup->append_single_option_line("line_width","quality_settings_line_width");
optgroup->append_single_option_line("initial_layer_line_width","quality_settings_line_width");
optgroup->append_single_option_line("outer_wall_line_width","quality_settings_line_width");
optgroup->append_single_option_line("inner_wall_line_width","quality_settings_line_width");
optgroup->append_single_option_line("top_surface_line_width","quality_settings_line_width");
optgroup->append_single_option_line("sparse_infill_line_width","quality_settings_line_width");
optgroup->append_single_option_line("internal_solid_infill_line_width","quality_settings_line_width");
optgroup->append_single_option_line("support_line_width","quality_settings_line_width");
optgroup->append_single_option_line("initial_layer_line_width","quality_settings_line_width#first-layer");
optgroup->append_single_option_line("outer_wall_line_width","quality_settings_line_width#outer-wall");
optgroup->append_single_option_line("inner_wall_line_width","quality_settings_line_width#inner-wall");
optgroup->append_single_option_line("top_surface_line_width","quality_settings_line_width#top-surface");
optgroup->append_single_option_line("sparse_infill_line_width","quality_settings_line_width#sparse-infill");
optgroup->append_single_option_line("internal_solid_infill_line_width","quality_settings_line_width#internal-solid-infill");
optgroup->append_single_option_line("support_line_width","quality_settings_line_width#support");
optgroup->append_single_option_line("bridge_line_width","quality_settings_line_width#bridge");
optgroup = page->new_optgroup(L("Seam"), L"param_seam");
optgroup->append_single_option_line("seam_position", "quality_settings_seam#seam-position");
@@ -2407,7 +2428,7 @@ void TabPrint::build()
optgroup = page->new_optgroup(L("Bridging"), L"param_bridge");
optgroup->append_single_option_line("bridge_flow", "quality_settings_bridging#flow-ratio");
optgroup->append_single_option_line("internal_bridge_flow", "quality_settings_bridging#flow-ratio");
optgroup->append_single_option_line("internal_bridge_flow", "quality_settings_bridging#flow-ratio");
optgroup->append_single_option_line("bridge_density", "quality_settings_bridging#bridge-density");
optgroup->append_single_option_line("internal_bridge_density", "quality_settings_bridging#bridge-density");
optgroup->append_single_option_line("thick_bridges", "quality_settings_bridging#thick-bridges");
@@ -2462,6 +2483,9 @@ void TabPrint::build()
optgroup->append_single_option_line("lateral_lattice_angle_1", "strength_settings_patterns#lateral-lattice");
optgroup->append_single_option_line("lateral_lattice_angle_2", "strength_settings_patterns#lateral-lattice");
optgroup->append_single_option_line("infill_overhang_angle", "strength_settings_patterns#lateral-honeycomb");
optgroup->append_single_option_line("lightning_overhang_angle", "strength_settings_patterns#lightning");
optgroup->append_single_option_line("lightning_prune_angle", "strength_settings_patterns#lightning");
optgroup->append_single_option_line("lightning_straightening_angle", "strength_settings_patterns#lightning");
optgroup->append_single_option_line("infill_anchor_max", "strength_settings_infill#anchor");
optgroup->append_single_option_line("infill_anchor", "strength_settings_infill#anchor");
optgroup->append_single_option_line("internal_solid_infill_pattern", "strength_settings_infill#internal-solid-infill");
@@ -2476,6 +2500,7 @@ void TabPrint::build()
optgroup->append_single_option_line("extra_solid_infills", "strength_settings_infill#extra-solid-infill");
optgroup->append_single_option_line("bridge_angle", "strength_settings_advanced#bridge-infill-direction");
optgroup->append_single_option_line("internal_bridge_angle", "strength_settings_advanced#bridge-infill-direction"); // ORCA: Internal bridge angle override
optgroup->append_single_option_line("relative_bridge_angle", "strength_settings_advanced#relative-bridge-angle");
optgroup->append_single_option_line("minimum_sparse_infill_area", "strength_settings_advanced#minimum-sparse-infill-threshold");
optgroup->append_single_option_line("infill_combination", "strength_settings_advanced#infill-combination");
optgroup->append_single_option_line("infill_combination_max_layer_height", "strength_settings_advanced#max-layer-height");
@@ -2642,9 +2667,12 @@ void TabPrint::build()
optgroup->append_single_option_line("single_extruder_multi_material_priming", "multimaterial_settings_prime_tower");
optgroup = page->new_optgroup(L("Filament for Features"), L"param_filament_for_features");
optgroup->append_single_option_line("wall_filament", "multimaterial_settings_filament_for_features#walls");
optgroup->append_single_option_line("sparse_infill_filament", "multimaterial_settings_filament_for_features#infill");
optgroup->append_single_option_line("solid_infill_filament", "multimaterial_settings_filament_for_features#solid-infill");
optgroup->append_single_option_line("outer_wall_filament_id", "multimaterial_settings_filament_for_features#outer-walls");
optgroup->append_single_option_line("inner_wall_filament_id", "multimaterial_settings_filament_for_features#inner-walls");
optgroup->append_single_option_line("sparse_infill_filament_id", "multimaterial_settings_filament_for_features#sparse-infill");
optgroup->append_single_option_line("internal_solid_filament_id", "multimaterial_settings_filament_for_features#internal-solid-infill");
optgroup->append_single_option_line("top_surface_filament_id", "multimaterial_settings_filament_for_features#top-surface");
optgroup->append_single_option_line("bottom_surface_filament_id", "multimaterial_settings_filament_for_features#bottom-surface");
optgroup->append_single_option_line("wipe_tower_filament", "multimaterial_settings_filament_for_features#wipe-tower");
optgroup = page->new_optgroup(L("Ooze prevention"), L"param_ooze_prevention");
@@ -4429,6 +4457,7 @@ void TabPrinter::build_fff()
create_line_with_widget(optgroup.get(), "printable_area", "custom-svg-and-png-bed-textures_124612", [this](wxWindow* parent) {
return create_bed_shape_widget(parent);
});
optgroup->append_single_option_line("parallel_printheads_count");
Option option = optgroup->get_option("bed_exclude_area");
option.opt.full_width = true;
optgroup->append_single_option_line(option, "printer_basic_information_printable_space#excluded-bed-area");
@@ -5409,6 +5438,7 @@ void TabPrinter::toggle_options()
// toggle_option("change_filament_gcode", have_multiple_extruders);
//}
if (m_active_page->title() == L("Basic information")) {
const auto &printer_cfg = m_preset_bundle->printers.get_edited_preset().config;
// SoftFever: hide BBL specific settings
for (auto el : {"scan_first_layer", "bbl_calib_mark_logo", "bbl_use_printhost"})
@@ -5420,6 +5450,9 @@ void TabPrinter::toggle_options()
auto gcf = m_config->option<ConfigOptionEnum<GCodeFlavor>>("gcode_flavor")->value;
toggle_line("enable_power_loss_recovery", is_BBL_printer || gcf == gcfMarlinFirmware);
const bool support_parallel_printheads = printer_cfg.opt_bool("support_parallel_printheads");
toggle_line("parallel_printheads_count", support_parallel_printheads);
}
@@ -5498,7 +5531,7 @@ void TabPrinter::toggle_options()
// some options only apply when not using firmware retraction
vec.resize(0);
vec = {"retraction_speed", "deretraction_speed", "retract_before_wipe",
"retract_length", "retract_restart_extra", "wipe",
"retract_length", "retract_restart_extra",
"wipe_distance"};
for (auto el : vec)
//BBS
@@ -5506,20 +5539,25 @@ void TabPrinter::toggle_options()
bool wipe = retraction && m_config->opt_bool("wipe", variant_index);
toggle_option("retract_before_wipe", wipe, i);
float retract_before_wipe = static_cast<ConfigOptionPercents*>(m_config->option("retract_before_wipe"))->values[variant_index];
if (use_firmware_retraction && wipe) {
if (use_firmware_retraction && wipe && retract_before_wipe < 100.0) {
//wxMessageDialog dialog(parent(),
MessageDialog dialog(parent(),
_(L("The Wipe option is not available when using the Firmware Retraction mode.\n"
"\nShall I disable it in order to enable Firmware Retraction?")),
_(L("The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n"
"\nShall I set it to 100% in order to enable Firmware Retraction?")),
_(L("Firmware Retraction")), wxICON_WARNING | wxYES | wxNO);
DynamicPrintConfig new_conf = *m_config;
if (dialog.ShowModal() == wxID_YES) {
auto wipe = static_cast<ConfigOptionBools*>(m_config->option("wipe")->clone());
for (size_t w = 0; w < wipe->values.size(); w++)
auto retract_before_wipe = static_cast<ConfigOptionPercents*>(m_config->option("retract_before_wipe")->clone());
for (size_t w = 0; w < wipe->values.size(); w++) {
wipe->values[w] = false;
retract_before_wipe->values[w] = 100.0;
}
new_conf.set_key_value("wipe", wipe);
new_conf.set_key_value("retract_before_wipe", retract_before_wipe);
}
else {
new_conf.set_key_value("use_firmware_retraction", new ConfigOptionBool(false));
+1 -5
View File
@@ -177,11 +177,7 @@ ZUserLogin::ZUserLogin(std::shared_ptr<ICloudServiceAgent> cloud_agent)
wxSize pSize = FromDIP(wxSize(650, 840));
SetSize(pSize);
int screenheight = wxSystemSettings::GetMetric(wxSYS_SCREEN_Y, NULL);
int screenwidth = wxSystemSettings::GetMetric(wxSYS_SCREEN_X, NULL);
int MaxY = (screenheight - pSize.y) > 0 ? (screenheight - pSize.y) / 2 : 0;
wxPoint tmpPT((screenwidth - pSize.x) / 2, MaxY);
Move(tmpPT);
CentreOnParent();
}
wxGetApp().UpdateDlgDarkUI(this);
}
+1 -1
View File
@@ -526,7 +526,7 @@ void WebViewPanel::SendCloudProvidersInfo()
json data;
json provider_array = json::array();
if (!app_config->get_stealth_mode()) {
if (!app_config->get_hide_login_side_panel()) {
auto providers = app_config->get_cloud_providers();
for (const auto& p : providers) {
provider_array.push_back(p);
+9 -2
View File
@@ -56,15 +56,22 @@ void SidePopup::Popup(wxWindow* focus)
}
if (focus) {
wxPoint pos = focus->ClientToScreen(wxPoint(0, -6));
int anchor_h = focus->GetSize().y + 12;
#ifdef __APPLE__
pos.x = pos.x - FromDIP(20);
// Orca #12936: since the wxWidgets 3.3 upgrade the transient popup is dismissed the
// instant the cursor enters the gap between the button and the menu, making
// "Print -> Export" unselectable. Anchor the menu flush against the button (slight
// overlap) so there is no dead-zone for the cursor to cross.
pos.y = focus->ClientToScreen(wxPoint(0, 0)).y;
anchor_h = focus->GetSize().y - 2;
#endif // __APPLE__
if (pos.x + max_width > screenwidth)
Position({pos.x - (pos.x + max_width - screenwidth),pos.y}, {0, focus->GetSize().y + 12});
Position({pos.x - (pos.x + max_width - screenwidth), pos.y}, {0, anchor_h});
else
Position(pos, {0, focus->GetSize().y + 12});
Position(pos, {0, anchor_h});
}
Slic3r::GUI::wxGetApp().set_side_menu_popup_status(true);
PopupWindow::Popup();
+667
View File
@@ -0,0 +1,667 @@
#include "3DPrinterOS.hpp"
#include <algorithm>
#include <sstream>
#include <exception>
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <wx/progdlg.h>
#include <wx/string.h>
#include <wx/event.h>
#include <wx/dialog.h>
#include <wx/radiobut.h>
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Utils.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/format.hpp"
#include "slic3r/GUI/GUI_Utils.hpp"
#include "slic3r/GUI/MsgDialog.hpp"
#include "slic3r/GUI/Widgets/ComboBox.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "Http.hpp"
#include <wx/busyinfo.h>
namespace fs = boost::filesystem;
namespace pt = boost::property_tree;
namespace {
class UploadOptionsDialog : public Slic3r::GUI::DPIDialog
{
public:
UploadOptionsDialog(wxWindow* parent,
const wxArrayString& cloud_projects,
const wxArrayString& cloud_printer_types,
const wxString preset_name)
: Slic3r::GUI::DPIDialog(parent,
wxID_ANY,
_L("3DPrinterOS Cloud upload options"),
wxDefaultPosition,
wxSize(100 * Slic3r::GUI::wxGetApp().em_unit(), -1),
wxDEFAULT_DIALOG_STYLE),
okButton(nullptr)
{
SetFont(Slic3r::GUI::wxGetApp().normal_font());
SetBackgroundColour(*wxWHITE);
SetForegroundColour(*wxBLACK);
singleRadio = new wxRadioButton(this, wxID_ANY, _L("Single file"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP);
projectRadio = new wxRadioButton(this, wxID_ANY, _L("Project File"));
projectsLabel = new wxStaticText(this, wxID_ANY, _L("Project:"));
wxStaticText* printerLabel = new wxStaticText(this, wxID_ANY, _L("Printer type:"));
projectsComboBox = new wxComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, DD_NO_CHECK_ICON);
printerTypeComboBox = new wxComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, DD_NO_CHECK_ICON | wxTE_READONLY);
printerWarningLabel = new wxStaticText(this, wxID_ANY, _L("Printer type not found, please select manually."));
printerWarningLabel->SetForegroundColour(*wxRED);
printerWarningLabel->Hide();
for (int i = 0; i < cloud_projects.size(); i++) {
projectsComboBox->Append(cloud_projects[i]);
}
if (cloud_printer_types.size() > 0) {
for (int i = 0; i < cloud_printer_types.size(); i++) {
printerTypeComboBox->Append(cloud_printer_types[i]);
if (cloud_printer_types[i].Find(preset_name) != wxNOT_FOUND && printerTypeComboBox->GetSelection() == -1) {
printerTypeComboBox->SetSelection(i);
}
}
if (printerTypeComboBox->GetCount() > 1) {
printerWarningLabel->Show();
} else {
printerTypeComboBox->SetSelection(0);
}
}
okButton = new wxButton(this, wxID_OK, _L("OK"));
wxButton* cancelButton = new wxButton(this, wxID_CANCEL, _L("Cancel"));
wxBoxSizer* radioSizer = new wxBoxSizer(wxHORIZONTAL);
wxBoxSizer* btnSizer = new wxBoxSizer(wxHORIZONTAL);
radioSizer->Add(singleRadio, 0, wxALL, 5);
radioSizer->Add(projectRadio, 0, wxALL, 5);
btnSizer->Add(okButton, 0, wxALL | wxALIGN_CENTER, 5);
btnSizer->Add(cancelButton, 0, wxALL | wxALIGN_CENTER, 5);
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
sizer->Add(radioSizer, 0, wxALL, 5);
sizer->Add(projectsLabel, 0, wxALL, 5);
sizer->Add(projectsComboBox, 0, wxALL | wxEXPAND, 5);
sizer->Add(printerLabel, 0, wxALL, 5);
sizer->Add(printerTypeComboBox, 0, wxALL | wxEXPAND, 5);
sizer->Add(printerWarningLabel, 0, wxLEFT | wxRIGHT | wxBOTTOM, 5);
sizer->Add(btnSizer, 0, wxALL | wxALIGN_CENTER, 5);
SetSizer(sizer);
sizer->Fit(this);
projectsComboBox->Hide();
projectsLabel->Hide();
projectRadio->Bind(wxEVT_RADIOBUTTON, &UploadOptionsDialog::OnRadioButtonSelected, this);
singleRadio->Bind(wxEVT_RADIOBUTTON, &UploadOptionsDialog::OnRadioButtonSelected, this);
// Bind combo box selection change to validation
printerTypeComboBox->Bind(wxEVT_COMBOBOX, &UploadOptionsDialog::OnPrinterTypeChanged, this);
ValidateOkButton(); // Initial validation
Slic3r::GUI::wxGetApp().UpdateDlgDarkUI(this);
CenterOnParent();
}
void OnRadioButtonSelected(wxCommandEvent& event)
{
wxRadioButton* selectedRadio = dynamic_cast<wxRadioButton*>(event.GetEventObject());
if (selectedRadio) {
wxString label = selectedRadio->GetLabel();
if (label == _L("Project File")) {
projectsComboBox->Show();
projectsLabel->Show();
} else {
projectsComboBox->Hide();
projectsLabel->Hide();
}
Layout();
}
}
void on_dpi_changed(const wxRect& suggested_rect) {}
void OnPrinterTypeChanged(wxCommandEvent& event)
{
ValidateOkButton();
event.Skip();
}
void ValidateOkButton()
{
bool hasSelection = (printerTypeComboBox->GetSelection() != wxNOT_FOUND);
okButton->Enable(hasSelection);
}
void GetValues(std::string& project, std::string& printer_type)
{
project = projectRadio->GetValue() ? std::string(projectsComboBox->GetValue().c_str()) : "";
printer_type = std::string(printerTypeComboBox->GetValue().c_str());
}
private:
wxComboBox* projectsComboBox;
wxComboBox* printerTypeComboBox;
wxStaticText* projectsLabel;
wxStaticText* printerWarningLabel;
wxRadioButton* singleRadio;
wxRadioButton* projectRadio;
wxButton* okButton;
};
class TokenAuthDialog : public Slic3r::GUI::DPIDialog
{
public:
TokenAuthDialog(wxWindow* parent, const std::string &url, const std::string& token, const std::string &cafile, pt::ptree& resp)
: Slic3r::GUI::DPIDialog(parent,
wxID_ANY,
"3DPrinterOS",
wxDefaultPosition,
wxSize(45 * Slic3r::GUI::wxGetApp().em_unit(), -1),
wxDEFAULT_DIALOG_STYLE)
, m_url(url)
, m_token(token)
, m_cafile(cafile)
, m_resp(resp)
{
SetFont(Slic3r::GUI::wxGetApp().normal_font());
SetBackgroundColour(*wxWHITE);
SetForegroundColour(*wxBLACK);
auto* sizer = new wxBoxSizer(wxVERTICAL);
sizer->Add(new wxStaticText(this, wxID_ANY, _L("Authorizing...")), 1, wxALL | wxCENTER, 10);
auto* cancelBtn = new wxButton(this, wxID_CANCEL, _L("Cancel"));
sizer->Add(cancelBtn, 0, wxALL | wxALIGN_CENTER, 10);
SetSizerAndFit(sizer);
Bind(wxEVT_THREAD, [this](wxThreadEvent& e) { EndModal(e.GetId()); });
Bind(wxEVT_TIMER, &TokenAuthDialog::OnRetry, this);
Bind(wxEVT_SHOW, &TokenAuthDialog::OnShow, this);
Bind(wxEVT_BUTTON, &TokenAuthDialog::OnCancel, this, wxID_CANCEL);
m_timer.SetOwner(this);
Slic3r::GUI::wxGetApp().UpdateDlgDarkUI(this);
CenterOnParent();
}
void on_dpi_changed(const wxRect& suggested_rect) {}
private:
void OnShow(wxShowEvent& event)
{
if (event.IsShown() && !m_started) {
m_started = true;
SendRequest();
}
event.Skip();
}
void OnCancel(wxCommandEvent&)
{
m_cancelled = true;
if (m_http_ptr) {
m_http_ptr->cancel(); // abort the background request
}
EndModal(wxID_CANCEL);
}
void OnRetry(wxTimerEvent&) { SendRequest(); }
void SendRequest()
{
if (m_cancelled || m_attempt >= m_max_retries) {
if (m_attempt >= m_max_retries) {
m_resp.put("result", false);
m_resp.put("message", "Maximum login retries exceeded");
}
wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT));
return;
}
m_attempt++;
std::string postBody = "token=" + m_token;
auto http = Slic3r::Http::post(m_url);
http.timeout_max(60);
if (!m_cafile.empty()) {
http.ca_file(m_cafile);
}
http.header("Content-Length", std::to_string(postBody.size()));
http.set_post_body(postBody);
http.on_error([this](std::string, std::string error, unsigned status) {
if (!m_cancelled) {
m_resp.put("result", false);
m_resp.put("message", (status != 200) ? "HTTP error: " + std::to_string(status) : error);
wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT));
}
})
.on_complete([this](std::string body, unsigned status) {
if (!m_cancelled) {
if (status != 200) {
m_resp.put("result", false);
m_resp.put("message", "HTTP error: " + std::to_string(status));
wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT));
return;
}
try {
std::stringstream ss(body);
pt::read_json(ss, m_resp);
} catch (...) {
m_resp.put("result", false);
m_resp.put("message", "Could not parse server response");
}
if (m_resp.get<bool>("result", false) && m_resp.get_optional<std::string>("message.session").has_value()) {
wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_OK));
} else if (m_resp.get<bool>("result", false)) {
if (m_attempt < m_max_retries)
m_timer.StartOnce(m_retry_delay_ms);
else
wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT));
} else {
wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT));
}
}
});
m_http_ptr = http.perform();
}
private:
std::string m_token;
std::string m_url;
std::string m_cafile;
pt::ptree& m_resp;
wxTimer m_timer;
std::shared_ptr<Slic3r::Http> m_http_ptr;
bool m_cancelled{false};
bool m_started{false};
int m_attempt{0};
const int m_max_retries{10};
const int m_retry_delay_ms{500};
};
} // namespace
namespace Slic3r {
static const std::string API_CREDENTIALS_PATH = "3dprinteros_api_cred.json";
C3DPrinterOS::C3DPrinterOS(DynamicPrintConfig *config)
: m_host(config->opt_string("print_host"))
, m_apikey(config->opt_string("printhost_apikey"))
, m_preset_name(config->opt_string("printer_model"))
{
m_api_session_file_path = (boost::filesystem::path(Slic3r::data_dir()) / API_CREDENTIALS_PATH)
.make_preferred()
.string();
load_api_session();
}
const char *C3DPrinterOS::get_name() const { return "3DPrinterOS"; }
bool C3DPrinterOS::test(wxString &msg) const
{
return check_session(msg);
}
bool C3DPrinterOS::login(wxString& msg) const
{
// Get token for auth
msg.clear();
std::string token = get_api_auth_token(msg);
if (token.empty()) {
msg = "Error. Can't get api token for authorization";
return false;
}
auto login_url = make_url("noauth/apiglobal_login_with_token/" + token);
wxLaunchDefaultBrowser(login_url);
pt::ptree login_resp;
login_with_token(login_resp, token);
std::string session, email;
try {
if (login_resp.get<bool>("result")) {
session = login_resp.get<std::string>("message.session");
email = login_resp.get<std::string>("message.email");
} else {
msg = wxString(login_resp.get<std::string>("message").c_str());
return false;
}
} catch (const std::exception&) {
msg = "Could not parse server response";
return false;
}
bool res = save_api_session(session, email);
if (!res) {
msg = "Error saving session to file";
}
return res;
}
wxString C3DPrinterOS::get_test_ok_msg() const
{
return _("Connection to 3DPrinterOS cloud works correctly.") + (!m_username.empty() ? "" + _(" Logined as user: ") + m_username : "");
}
wxString C3DPrinterOS::get_test_failed_msg(wxString &msg) const
{
return GUI::format_wxstr("%s: %s\n\n", _L("Error session check"), msg);
}
bool C3DPrinterOS::upload(
PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn
) const
{
const char *name = get_name();
const auto upload_filename = upload_data.upload_path.filename();
const auto upload_parent_path = upload_data.upload_path.parent_path();
wxString test_msg;
if (!check_session(test_msg)) {
error_fn(std::move(test_msg));
return false;
}
pt::ptree cloud_project_resp;
pt::ptree cloud_printer_types_resp;
get_cloud_projects_list(cloud_project_resp);
get_cloud_printer_types(cloud_printer_types_resp, m_preset_name);
wxArrayString cloud_projects_list;
wxArrayString cloud_printer_types_list;
try {
if (cloud_project_resp.get<bool>("result")) {
for (const auto &messageItem : cloud_project_resp.get_child("message")) {
cloud_projects_list.Add(messageItem.second.get<std::string>("name"));
}
}
if (cloud_printer_types_resp.get<bool>("result")) {
for (const auto &messageItem : cloud_printer_types_resp.get_child("message")) {
cloud_printer_types_list.Add(messageItem.second.get<std::string>("description"));
}
}
} catch (const std::exception &) {
error_fn("Could not parse server response");
return false;
}
// Show "Confirm cloud printer type and project for 3DPrinterOS upload
UploadOptionsDialog dlg(GUI::wxGetApp().GetTopWindow(), cloud_projects_list, cloud_printer_types_list, m_preset_name);
if (dlg.ShowModal() != wxID_OK) {
error_fn("Canceled");
return false;
}
std::string selected_project;
std::string selected_printer_type;
dlg.GetValues(selected_project, selected_printer_type);
std::string project_id;
std::string printer_type_id;
// search for cloud project_id by name
if (!selected_project.empty()) {
for (const auto& messageItem : cloud_project_resp.get_child("message")) {
if (messageItem.second.get<std::string>("name", "") == selected_project) {
project_id = messageItem.second.get<std::string>("id", "");
break;
}
}
}
// search for cloud printer_type_id by name
for (const auto& messageItem : cloud_printer_types_resp.get_child("message")) {
if (messageItem.second.get<std::string>("description", "") == selected_printer_type) {
printer_type_id = messageItem.second.get<std::string>("id", "");
break;
}
}
bool res = true;
auto url = make_url("apiglobal/upload");
std::string file_id;
pt::ptree uploadResponse;
auto http = Http::post(std::move(url));
if (!m_cafile.empty()) {
http.ca_file(m_cafile);
}
http.form_add("session", m_apikey)
.form_add("upload_type_id", "7")
.form_add("upload_soft_name", "OrcaSlicer")
.form_add("zip", "false")
.form_add_file("file", upload_data.source_path.string(), upload_filename.string());
if (!project_id.empty()) {
http.form_add("project_id", project_id);
} else if (!selected_project.empty()) {
http.form_add("project_name", selected_project);
http.form_add("project_color", "grey");
}
http.on_complete([&](std::string body, unsigned status) {
std::stringstream ss(body);
try {
pt::read_json(ss, uploadResponse);
} catch (const std::exception &) {
uploadResponse.put("result", false);
uploadResponse.put("message", "Could not parse server response");
}
})
.on_error([&](std::string body, std::string error, unsigned status) {
error_fn(format_error(body, error, status));
res = false;
})
.on_progress([&](Http::Progress progress, bool &cancel) {
prorgess_fn(std::move(progress), cancel);
if (cancel) {
res = false;
}
})
.perform_sync();
try {
if (uploadResponse.get<bool>("result")) {
file_id = uploadResponse.get<std::string>("message.file_id");
} else {
res = false;
error_fn(uploadResponse.get<std::string>("message"));
}
} catch (const std::exception &) {
res = false;
error_fn("Error during file upload");
}
// set printer type for uploaded gcode
if (res) {
pt::ptree update_file_response;
update_file(update_file_response, file_id, printer_type_id, "OrcaSlicer");
try {
if (!update_file_response.get<bool>("result")) {
const std::string msg = update_file_response.get<std::string>("message", "Unknown update error");
BOOST_LOG_TRIVIAL(warning) << "Failed to update uploaded file: " << msg;
}
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "Could not parse update response: " << ex.what();
}
if (upload_data.post_action == PrintHostPostUploadAction::StartPrint && !upload_data.use_3mf) {
auto quick_print_url = make_url("quickprint?file_id=" + file_id);
wxLaunchDefaultBrowser(quick_print_url);
}
}
return res;
}
void C3DPrinterOS::log_out() const
{
boost::filesystem::remove(m_api_session_file_path.c_str());
}
bool C3DPrinterOS::validate_version_text(const boost::optional<std::string> &version_text) const
{
return version_text ? boost::starts_with(*version_text, "3DPrinterOS") : true;
}
std::string C3DPrinterOS::make_url(const std::string &path) const
{
if (m_host.find("http://") == 0 || m_host.find("https://") == 0) {
if (m_host.back() == '/') {
return (boost::format("%1%%2%") % m_host % path).str();
} else {
return (boost::format("%1%/%2%") % m_host % path).str();
}
} else {
return (boost::format("https://%1%/%2%") % m_host % path).str();
}
}
std::string C3DPrinterOS::get_api_auth_token(wxString &err) const
{
std::string result;
pt::ptree resp;
std::string postBody = "app_type=plugin&app_name=" + Http::url_encode("OrcaSlicer");
send_form("apiglobal/generate_login_token", postBody, resp);
try {
if (resp.get<bool>("result")) {
result = resp.get<std::string>("message");
} else {
err = wxString(resp.get<std::string>("message").c_str());
}
} catch (const std::exception &) {
err = "Could not parse server response";
}
return result;
}
void C3DPrinterOS::login_with_token(pt::ptree &resp, const std::string &token) const {
auto url = make_url("apiglobal/login_with_token");
TokenAuthDialog dlg(GUI::wxGetApp().GetTopWindow(), url, token, m_cafile, resp);
dlg.ShowModal();
}
bool C3DPrinterOS::check_session(wxString &msg) const {
std::string postBody = "session=" + m_apikey;
pt::ptree resp;
send_form("apiglobal/check_session", postBody, resp);
try {
if (resp.get<bool>("result")) {
return true;
} else {
msg = wxString(resp.get<std::string>("message").c_str());
return false;
}
} catch (const std::exception &) {
msg = wxString("Could not parse server response");
return false;
}
return false;
}
bool C3DPrinterOS::save_api_session(const std::string &session, const std::string &email) const {
pt::ptree j;
j.put("session", session);
j.put("email", email);
try {
auto temp_path = m_api_session_file_path + ".tmp";
pt::write_json(temp_path, j);
boost::filesystem::rename(temp_path, m_api_session_file_path);
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to write json to file. Path = "
<< m_api_session_file_path
<< " Reason = " << err.what();
return false;
}
return true;
}
void C3DPrinterOS::load_api_session()
{
m_apikey.clear();
if (boost::filesystem::exists(m_api_session_file_path)) {
pt::ptree j;
try {
pt::read_json(m_api_session_file_path, j);
m_apikey = j.get<std::string>("session");
m_username = j.get<std::string>("email");
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": load_api_session failed, reason = " << err.what();
// remove corrupted file to avoid repeated failures
try {
boost::filesystem::remove(m_api_session_file_path);
} catch (...) {}
}
};
}
void C3DPrinterOS::send_form(
const std::string &endpoint,
const std::string &postBody,
boost::property_tree::ptree &responseTree
) const
{
responseTree.clear();
auto url = make_url(endpoint);
auto http = Http::post(std::move(url));
if (!m_cafile.empty()) {
http.ca_file(m_cafile);
}
http.header("Content-length", std::to_string(postBody.size()));
http.set_post_body(postBody);
http.on_error([&](std::string body, std::string error, unsigned status) {
BOOST_LOG_TRIVIAL(error) << boost::format("Error sending form: %1%") % error;
responseTree.put("result", false);
responseTree.put("message", error);
})
.on_complete([&, this](std::string body, unsigned) {
std::stringstream ss(body);
try {
pt::read_json(ss, responseTree);
} catch (const std::exception &) {
responseTree.put("result", false);
responseTree.put("message", "Could not parse server response");
}
})
.perform_sync();
}
void C3DPrinterOS::get_cloud_projects_list(boost::property_tree::ptree &response) const
{
std::string postBody = std::string("session=" + m_apikey);
send_form("apiglobal/get_projects", postBody, response);
}
void C3DPrinterOS::get_cloud_printer_types(boost::property_tree::ptree &response, const std::string &query) const
{
std::string postBody = std::string("session=" + m_apikey);
if (!query.empty()) {
postBody += "&description=" + Http::url_encode(query) + "&software_version=" + Http::url_encode("OrcaSlicer");
}
send_form("apiglobal/get_printer_types", postBody, response);
}
void C3DPrinterOS::update_file(boost::property_tree::ptree &response, const std::string &file_id, const std::string &ptype, const std::string &gtype) const
{
std::string postBody = "session=" + m_apikey
+ "&updates[" + file_id + "][ptype]=" + ptype
+ "&updates[" + file_id + "][gtype]=" + Http::url_encode(gtype)
+ "&updates[" + file_id + "][zip]=false";
send_form("apiglobal/file_update", postBody, response);
}
};
// namespace Slic3r
+80
View File
@@ -0,0 +1,80 @@
#ifndef slic3r_3DPrinterOS_hpp_
#define slic3r_3DPrinterOS_hpp_
#include <string>
#include <wx/string.h>
#include <boost/optional.hpp>
#include <boost/property_tree/ptree.hpp>
#include "PrintHost.hpp"
#include "slic3r/GUI/GUI.hpp"
namespace Slic3r {
class DynamicPrintConfig;
class Http;
class C3DPrinterOS : public PrintHost
{
public:
C3DPrinterOS(DynamicPrintConfig *config);
~C3DPrinterOS() override = default;
const char* get_name() const override;
bool test(wxString &curl_msg) const override;
bool login(wxString &msg) const;
wxString get_test_ok_msg () const override;
wxString get_test_failed_msg (wxString &msg) const override;
bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override;
bool has_auto_discovery() const override { return false; }
bool can_test() const override { return true; }
bool is_cloud() const override { return true; }
void log_out() const override;
bool is_logged_in() const override { return !m_apikey.empty(); }
PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint | PrintHostPostUploadAction::QueuePrint; }
std::string get_host() const override { return m_host; }
static std::string default_host() { return "https://cloud.3dprinteros.com"; }
protected:
bool validate_version_text(const boost::optional<std::string> &version_text) const;
private:
std::string m_host;
std::string m_apikey;
std::string m_cafile;
std::string m_username;
std::string m_host_type;
std::string m_preset_name;
std::string m_api_session_file_path;
void load_api_session();
bool save_api_session(const std::string &session, const std::string &email) const;
std::string parse_printer_model(const std::string& input) const;
std::string make_url(const std::string &path) const;
std::string get_api_auth_token(wxString &err) const;
void login_with_token(boost::property_tree::ptree &resp, const std::string &token) const;
bool check_session(wxString &msg) const;
void send_form(
const std::string &endpoint,
const std::string &postBody,
boost::property_tree::ptree &responseTree
) const;
void get_cloud_projects_list(boost::property_tree::ptree &response) const;
void get_cloud_printer_types(boost::property_tree::ptree &response, const std::string &querry) const;
void update_file(
boost::property_tree::ptree &response,
const std::string &file_id,
const std::string &ptype,
const std::string &gtype
) const;
};
}
#endif
+1 -1
View File
@@ -433,7 +433,7 @@ std::string BBLCloudServiceAgent::request_setting_id(std::string name, std::map<
return "";
}
int BBLCloudServiceAgent::put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code)
int BBLCloudServiceAgent::put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, bool force)
{
auto& plugin = BBLNetworkPlugin::instance();
auto agent = plugin.get_agent();
+1 -1
View File
@@ -70,7 +70,7 @@ public:
// Settings Synchronization
int get_user_presets(std::map<std::string, std::map<std::string, std::string>>* user_presets) override;
std::string request_setting_id(std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code) override;
int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code) override;
int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, bool force = false) override;
int get_setting_list(std::string bundle_version, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) override;
int get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) override;
int delete_setting(std::string setting_id) override;
+106 -30
View File
@@ -1,6 +1,8 @@
#include "ElegooLink.hpp"
#include <algorithm>
#include <map>
#include <mutex>
#include <sstream>
#include <exception>
#include <boost/format.hpp>
@@ -60,6 +62,52 @@ namespace Slic3r {
namespace {
constexpr const char* ELEGOO_CC2_DEFAULT_TOKEN = "123456";
// AppConfig section for CC2 serial numbers, keyed by normalized print_host (host/IP).
constexpr const char* ELEGOO_DEV_SN_SECTION = "dev_sn";
static std::mutex s_sn_cache_mutex;
static std::map<std::string, std::string> s_sn_cache;
std::string sn_cache_key(const std::string& host_ip, const std::string& token)
{
return host_ip + ":" + token;
}
void cache_sn(const std::string& host_ip, const std::string& token, const std::string& sn)
{
if (host_ip.empty() || token.empty() || sn.empty())
return;
std::lock_guard<std::mutex> lock(s_sn_cache_mutex);
s_sn_cache[sn_cache_key(host_ip, token)] = sn;
}
std::string lookup_sn(const std::string& host_ip, const std::string& token)
{
std::lock_guard<std::mutex> lock(s_sn_cache_mutex);
auto it = s_sn_cache.find(sn_cache_key(host_ip, token));
return it != s_sn_cache.end() ? it->second : std::string{};
}
std::string load_sn_from_config(const std::string& host_ip)
{
if (host_ip.empty())
return {};
AppConfig* app_cfg = GUI::get_app_config();
if (app_cfg == nullptr)
return {};
return app_cfg->get(ELEGOO_DEV_SN_SECTION, host_ip);
}
void persist_sn(const std::string& host_ip, const std::string& token, const std::string& sn)
{
if (host_ip.empty() || sn.empty())
return;
cache_sn(host_ip, token, sn);
AppConfig* app_cfg = GUI::get_app_config();
if (app_cfg == nullptr)
return;
app_cfg->set_str(ELEGOO_DEV_SN_SECTION, host_ip, sn);
}
enum class ElegooPrinterType {
Other,
@@ -122,6 +170,36 @@ namespace Slic3r {
}
}
// NOTE (merge): host parsing was moved into Http::get_host_from_url /
// Http::get_host_header_value by the K2 discovery refactor on this branch, so the
// former ElegooLink-local get_host_from_url/get_host_from_url_no_port helpers are gone.
// main only added the CC2 serial-number lookup below; it is kept here and routed through
// Http::get_host_header_value, which has the same host:port semantics the SN cache key
// relies on.
std::string lookup_cc2_serial_impl(const std::string& printer_model,
const std::string& print_host,
const std::string& apikey)
{
if (classify_printer_model(printer_model) != ElegooPrinterType::CC2)
return {};
const std::string host_ip = Http::get_host_header_value(print_host);
const std::string token = get_cc2_token(apikey);
std::string sn = lookup_sn(host_ip, token);
if (sn.empty())
sn = load_sn_from_config(host_ip);
return sn;
}
std::string lookup_cc2_serial(DynamicPrintConfig* config)
{
if (config == nullptr)
return {};
return lookup_cc2_serial_impl(config->opt_string("printer_model"),
config->opt_string("print_host"),
config->opt_string("printhost_apikey"));
}
#ifdef WIN32
// Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail.
std::string substitute_host(const std::string& orig_addr, std::string sub_addr)
@@ -262,11 +340,32 @@ namespace Slic3r {
if (classify_printer_model(config->opt_string("printer_model")) != ElegooPrinterType::CC2)
return fallback_webui;
std::string web_path = resources_dir() + "/plugins/elegoolink/web/lan_service_web/index.html";
std::string web_path = resources_dir() + "/web/elegoolink/lan_service_web/index.html";
std::replace(web_path.begin(), web_path.end(), '\\', '/');
web_path = "file://" + web_path;
web_path += "?access_code=" + get_cc2_token(config->opt_string("printhost_apikey"));
web_path += "&ip=" + Http::get_host_header_value(host) + "&id=elegoo_123456";
const std::string token = get_cc2_token(config->opt_string("printhost_apikey"));
const std::string host_ip = Http::get_host_header_value(host);
// Pass sn= so the panel can subscribe to the correct MQTT topics.
std::string sn = lookup_cc2_serial(config);
if (sn.empty()) {
std::string error_msg;
auto http = Http::get("http://" + host_ip + "/system/info?X-Token=" + escape_string(token));
http.timeout_connect(3).timeout_max(5);
http.header("X-Token", token);
http.header("Accept", "application/json");
http.on_complete([&](std::string body, unsigned /*status*/) {
parse_cc2_response(body, error_msg, &sn);
}).perform_sync();
if (!sn.empty())
persist_sn(host_ip, token, sn);
}
web_path += "?access_code=" + token;
web_path += "&ip=" + host_ip;
if (!sn.empty())
web_path += "&sn=" + sn;
web_path += "&id=elegoo_123456";
const std::string lang = GUI::wxGetApp().current_language_code_safe().utf8_string();
if (!lang.empty())
@@ -305,33 +404,9 @@ namespace Slic3r {
std::string ElegooLink::get_sn() const
{
if (classify_printer_model(m_printerModel) != ElegooPrinterType::CC2)
return "";
const char* name = get_name();
std::string sn;
const auto token = cc2_token();
auto http = Http::get(make_cc2_info_url());
http.timeout_connect(10)
.timeout_max(15);
http.header("X-Token", token);
http.header("Accept", "application/json");
http.on_error([&](std::string body, std::string error, unsigned status) {
BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting CC2 device info for SN: %2%, HTTP %3%, body: `%4%`") % name % error % status % body;
})
.on_complete([&](std::string body, unsigned status) {
std::string error_message;
if (!parse_cc2_response(body, error_message, &sn)) {
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: Failed to parse CC2 SN response, HTTP %2%, reason: %3%") % name % status % error_message;
sn.clear();
}
})
#ifdef WIN32
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
#endif // WIN32
.perform_sync();
return sn;
// Panel IPC calls this on every load with a 10s timeout. Never block on HTTP
// here — URL sn= and dev_sn must be enough; HTTP is only for get_print_host_webui.
return lookup_cc2_serial_impl(m_printerModel, m_host, m_apikey);
}
bool ElegooLink::elegoo_test(wxString& msg) const{
@@ -410,6 +485,7 @@ namespace Slic3r {
msg = format_error(body, error_message.empty() ? "CC2 device not detected" : error_message, status);
return;
}
persist_sn(Http::get_host_header_value(m_host), token, serial_number);
res = true;
})
#ifdef WIN32
+1 -1
View File
@@ -247,7 +247,7 @@ public:
/**
* Update or create a preset with a known setting_id.
*/
virtual int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code) = 0;
virtual int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, bool force = false) = 0;
/**
* Trigger bulk download of user presets.
+311
View File
@@ -0,0 +1,311 @@
#include "Moonraker.hpp"
#include <sstream>
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include "libslic3r/PrintConfig.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/format.hpp"
#include "Http.hpp"
namespace pt = boost::property_tree;
namespace Slic3r {
Moonraker::Moonraker(DynamicPrintConfig *config)
: m_host(config->opt_string("print_host"))
, m_apikey(config->opt_string("printhost_apikey"))
, m_cafile(config->opt_string("printhost_cafile"))
, m_ssl_revoke_best_effort(config->opt_bool("printhost_ssl_ignore_revoke"))
{}
const char* Moonraker::get_name() const { return "Moonraker"; }
wxString Moonraker::get_test_ok_msg() const
{
return _(L("Connection to Moonraker is working correctly."));
}
wxString Moonraker::get_test_failed_msg(wxString &msg) const
{
return GUI::format_wxstr("%s: %s", _L("Could not connect to Moonraker"), msg);
}
std::string Moonraker::make_url(const std::string &path) const
{
if (m_host.find("http://") == 0 || m_host.find("https://") == 0) {
if (m_host.back() == '/')
return (boost::format("%1%%2%") % m_host % path).str();
return (boost::format("%1%/%2%") % m_host % path).str();
}
return (boost::format("http://%1%/%2%") % m_host % path).str();
}
void Moonraker::set_auth(Http &http) const
{
//ORCA: Moonraker accepts unauthenticated requests by default; X-Api-Key is the only auth header
// defined by the Moonraker spec. HTTP Basic / Digest do NOT belong here even if the user
// filled the user/password fields — those are PrusaLink/OctoPrint conventions.
if (!m_apikey.empty())
http.header("X-Api-Key", m_apikey);
if (!m_cafile.empty())
http.ca_file(m_cafile);
}
bool Moonraker::test(wxString &msg) const
{
//ORCA: Moonraker's /server/info returns
// { "result": { "klippy_state": "ready|startup|shutdown|error|disconnected", ... } }
// We treat the connection as healthy as long as the envelope is valid and `klippy_state`
// is present — matching the OctoPrint/PrusaLink convention of "can I reach this host?".
// Klipper state (idle, error, etc.) is surfaced to the log but does not gate the test:
// buddy-fork firmwares legitimately report non-`ready` states at idle, and any real upload
// problem will surface a contextual error at upload() time anyway.
const char *name = get_name();
bool res = true;
auto url = make_url("server/info");
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get server info at: %2%") % name % url;
auto http = Http::get(std::move(url));
set_auth(http);
http.on_error([&](std::string body, std::string error, unsigned status) {
BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting server info: %2%, HTTP %3%, body: `%4%`")
% name % error % status % body;
res = false;
msg = format_error(body, error, status);
})
.on_complete([&, this](std::string body, unsigned) {
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: /server/info body: %2%") % name % body;
try {
std::stringstream ss(body);
pt::ptree ptree;
pt::read_json(ss, ptree);
const auto klippy_state = ptree.get_optional<std::string>("result.klippy_state");
if (!klippy_state) {
//ORCA: response wasn't shaped like a Moonraker /server/info reply — likely an OctoPrint
// or PrusaLink host the user mis-selected as Moonraker, or a totally different
// service. Treat as a connection failure with a clear hint.
res = false;
msg = _L("The host responded but it doesn't look like Moonraker (missing result.klippy_state).");
return;
}
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: klippy_state = %2%") % name % (*klippy_state);
} catch (const std::exception &ex) {
res = false;
msg = GUI::format_wxstr(_L("Could not parse Moonraker server response: %s"), ex.what());
}
})
#ifdef WIN32
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
#endif
.perform_sync();
return res;
}
bool Moonraker::get_storage(wxArrayString &storage_path, wxArrayString &storage_name) const
{
//ORCA: GET /server/files/roots enumerates Moonraker's storage roots (default "gcodes" plus any
// configured extras like "config", "logs", "timelapse"). Only roots with permissions
// including "rw" or "rwd" can receive uploads; we filter to those so the UI dropdown only
// offers usable destinations. The base class returns false (no per-host storage); returning
// true here populates the storage picker in PrintHostDialogs's send-to-print dialog.
// Failures (404 — older Moonraker, or a buddy-fork that doesn't implement the endpoint)
// gracefully degrade to false so upload() falls back to the hardcoded "gcodes" default.
const char *name = get_name();
bool got_any = false;
auto url = make_url("server/files/roots");
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Enumerating storage roots at: %2%") % name % url;
auto http = Http::get(std::move(url));
set_auth(http);
http.on_error([&](std::string body, std::string error, unsigned status) {
//ORCA: /server/files/roots is optional in the Moonraker spec and absent on older versions
// and slimmer shims (e.g. Prusa-Firmware-Buddy 0.8.x prusalink-shim returns 501). A
// missing endpoint here is benign — upload() silently falls back to the hardcoded
// "gcodes" root — so don't pollute the log at warning level for it. Other HTTP
// errors still warn.
if (status == 404 || status == 501) {
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: /server/files/roots not implemented (HTTP %2%); upload() will fall back to the \"gcodes\" root.")
% name % status;
} else {
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: Could not enumerate roots: %2%, HTTP %3%, body: `%4%`")
% name % error % status % body;
}
})
.on_complete([&, this](std::string body, unsigned) {
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: /server/files/roots body: %2%") % name % body;
try {
std::stringstream ss(body);
pt::ptree ptree;
pt::read_json(ss, ptree);
const auto result_node = ptree.get_child_optional("result");
if (!result_node)
return;
for (const auto &child : *result_node) {
const std::string &root = child.second.get<std::string>("name", "");
const std::string &perms = child.second.get<std::string>("permissions", "");
if (root.empty() || perms.find('w') == std::string::npos)
continue;
storage_path.Add(wxString::FromUTF8(root));
storage_name.Add(wxString::FromUTF8(root));
got_any = true;
}
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: Could not parse roots: %2%") % name % ex.what();
}
})
#ifdef WIN32
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
#endif
.perform_sync();
return got_any;
}
bool Moonraker::start_print(wxString &error_msg, const std::string &filename) const
{
//ORCA: POST /printer/print/start with JSON body { "filename": "<name>.gcode" }.
// `filename` is what /server/files/upload returned as result.item.path (the storage-relative
// path inside `root`, no leading slash, with extension). Build the body via property_tree
// so that special characters in the filename (server-side collision-suffix could produce
// paths with quotes / backslashes on exotic file systems) are properly escaped.
const char *name = get_name();
bool res = true;
auto url = make_url("printer/print/start");
pt::ptree body_tree;
body_tree.put("filename", filename);
std::ostringstream body_ss;
pt::write_json(body_ss, body_tree, /*pretty=*/false);
std::string body = body_ss.str();
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Starting print of %2% at %3%") % name % filename % url;
auto http = Http::post(std::move(url));
set_auth(http);
http.header("Content-Type", "application/json")
.set_post_body(body)
.on_complete([&](std::string body, unsigned status) {
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: print/start HTTP %2%: %3%") % name % status % body;
})
.on_error([&](std::string body, std::string error, unsigned status) {
BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error starting print at %2%: %3%, HTTP %4%, body: `%5%`")
% name % url % error % status % body;
res = false;
error_msg = format_error(body, error, status);
})
#ifdef WIN32
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
#endif
.perform_sync();
return res;
}
bool Moonraker::upload(PrintHostUpload upload_data, ProgressFn progress_fn, ErrorFn error_fn, InfoFn info_fn) const
{
//ORCA: POST /server/files/upload as multipart/form-data with:
// file = <gcode file>
// root = <storage root> (Moonraker default: "gcodes")
// Successful response shape:
// { "result": { "item": { "path": "<name>.gcode", "root": "<root>" }, "print_started": <bool> } }
// We always start the print explicitly via /printer/print/start regardless of `print_started`
// so the user can rely on a single call site for state.
wxString test_msg;
if (!test(test_msg)) {
error_fn(std::move(test_msg));
return false;
}
const char *name = get_name();
const auto upload_filename = upload_data.upload_path.filename();
const auto upload_parent_path = upload_data.upload_path.parent_path();
//ORCA: upload_data.storage is plumbed from the (future) per-printer storage dropdown. When unset,
// fall back to the Moonraker-standard "gcodes" root. Reading it through here means a UI
// addition later (storage picker) needs no change to this method.
const std::string root = upload_data.storage.empty() ? std::string("gcodes") : upload_data.storage;
std::string url = make_url("server/files/upload");
bool result = true;
std::string uploaded_path;
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% to %3% (root=%4%, filename=%5%, start_print=%6%)")
% name
% upload_data.source_path
% url
% root
% upload_filename.string()
% (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false");
auto http = Http::post(std::move(url));
set_auth(http);
http.form_add("root", root)
.form_add_file("file", upload_data.source_path.string(), upload_filename.string())
.on_complete([&](std::string body, unsigned status) {
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: upload HTTP %2%: %3%") % name % status % body;
try {
std::stringstream ss(body);
pt::ptree ptree;
pt::read_json(ss, ptree);
//ORCA: Moonraker confirms the storage-relative path in result.item.path. We pass exactly
// that string to /printer/print/start so any server-side renaming (collision suffix,
// etc.) is respected.
const auto stored_path = ptree.get_optional<std::string>("result.item.path");
if (stored_path) {
uploaded_path = *stored_path;
} else {
//ORCA: fallback if the server response omits result.item.path (older Moonraker, or
// a buddy-fork that returns a slimmer envelope). Use the original filename.
uploaded_path = upload_filename.string();
BOOST_LOG_TRIVIAL(warning) << boost::format(
"%1%: upload response missing result.item.path, falling back to original filename `%2%`")
% name % uploaded_path;
}
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(warning) << boost::format(
"%1%: could not parse upload response (%2%); falling back to original filename")
% name % ex.what();
uploaded_path = upload_filename.string();
}
})
.on_error([&](std::string body, std::string error, unsigned status) {
BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading to %2%: %3%, HTTP %4%, body: `%5%`")
% name % url % error % status % body;
error_fn(format_error(body, error, status));
result = false;
})
.on_progress([&](Http::Progress progress, bool &cancel) {
progress_fn(std::move(progress), cancel);
if (cancel) {
BOOST_LOG_TRIVIAL(info) << name << ": Upload canceled";
result = false;
}
})
#ifdef WIN32
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
#endif
.perform_sync();
if (!result)
return false;
if (upload_data.post_action == PrintHostPostUploadAction::StartPrint && !uploaded_path.empty()) {
wxString start_msg;
if (!start_print(start_msg, uploaded_path)) {
error_fn(std::move(start_msg));
return false;
}
}
return true;
}
}
+63
View File
@@ -0,0 +1,63 @@
#ifndef slic3r_Moonraker_hpp_
#define slic3r_Moonraker_hpp_
#include <string>
#include <wx/string.h>
#include <wx/arrstr.h>
#include "PrintHost.hpp"
#include "libslic3r/PrintConfig.hpp"
namespace Slic3r {
class DynamicPrintConfig;
class Http;
// Moonraker is the JSON / WebSocket gateway that ships in front of Klipper
// (and on Klipper-API-compatible firmwares like the Prusa-Firmware-Buddy
// Buddy-Klipper fork). REST shape differs from OctoPrint: distinct paths,
// JSON body for print/start, {"result":...}/{"error":...} envelope.
//
// Endpoints used:
// GET /server/info -- connection test, reads klippy_state
// POST /server/files/upload (multipart) -- upload gcode (form fields: file, root)
// POST /printer/print/start (json) -- {"filename":"<name>.gcode"} starts print
//
// Auth: X-Api-Key header if `printhost_apikey` is non-empty; Moonraker accepts
// unauthenticated LAN access by default, so the key is optional. HTTP Basic /
// Digest are not part of the Moonraker spec and are not sent.
class Moonraker : public PrintHost
{
public:
Moonraker(DynamicPrintConfig *config);
~Moonraker() override = default;
const char* get_name() const override;
bool test(wxString &curl_msg) const override;
wxString get_test_ok_msg() const override;
wxString get_test_failed_msg(wxString &msg) const override;
bool upload(PrintHostUpload upload_data, ProgressFn progress_fn, ErrorFn error_fn, InfoFn info_fn) const override;
bool has_auto_discovery() const override { return false; }
bool can_test() const override { return true; }
PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; }
std::string get_host() const override { return m_host; }
bool get_storage(wxArrayString &storage_path, wxArrayString &storage_name) const override;
const std::string& get_apikey() const { return m_apikey; }
const std::string& get_cafile() const { return m_cafile; }
protected:
std::string m_host;
std::string m_apikey;
std::string m_cafile;
bool m_ssl_revoke_best_effort;
void set_auth(Http &http) const;
std::string make_url(const std::string &path) const;
bool start_print(wxString &error_msg, const std::string &filename) const;
};
}
#endif
+11 -9
View File
@@ -383,11 +383,12 @@ int NetworkAgent::put_setting(std::string setting_id,
std::string name,
std::map<std::string, std::string>* values_map,
unsigned int* http_code,
const std::string& provider)
const std::string& provider,
bool force)
{
const auto cloud_agent = get_cloud_agent(provider);
if (cloud_agent)
return cloud_agent->put_setting(std::move(setting_id), std::move(name), values_map, http_code);
return cloud_agent->put_setting(std::move(setting_id), std::move(name), values_map, http_code, force);
return -1;
}
@@ -582,21 +583,22 @@ int NetworkAgent::get_my_token(std::string ticket, unsigned int* http_code, std:
return -1;
}
int NetworkAgent::track_enable(bool enable, const std::string& provider)
int NetworkAgent::track_enable(bool enable)
{
this->enable_track = enable;
const auto cloud_agent = get_cloud_agent(provider);
// Orca cloud has no telemetry; the only cloud agent that tracks events is BBL.
this->enable_track = enable;
const auto cloud_agent = get_cloud_agent(BBL_CLOUD_PROVIDER);
if (cloud_agent)
return cloud_agent->track_enable(enable);
return -1;
return 0;
}
int NetworkAgent::track_remove_files(const std::string& provider)
int NetworkAgent::track_remove_files()
{
const auto cloud_agent = get_cloud_agent(provider);
const auto cloud_agent = get_cloud_agent(BBL_CLOUD_PROVIDER);
if (cloud_agent)
return cloud_agent->track_remove_files();
return -1;
return 0;
}
int NetworkAgent::track_event(std::string evt_key, std::string content, const std::string& provider)
+4 -3
View File
@@ -93,7 +93,7 @@ public:
// NOTE: this should always call only OrcaCloud
int get_user_presets(std::map<std::string, std::map<std::string, std::string>>* user_presets, const std::string& provider = ORCA_CLOUD_PROVIDER);
std::string request_setting_id(std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, const std::string& provider = ORCA_CLOUD_PROVIDER);
int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, const std::string& provider = ORCA_CLOUD_PROVIDER);
int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, const std::string& provider = ORCA_CLOUD_PROVIDER, bool force = false);
int get_setting_list(std::string bundle_version, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr, const std::string& provider = ORCA_CLOUD_PROVIDER);
int get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr, const std::string& provider = ORCA_CLOUD_PROVIDER);
int delete_setting(std::string setting_id, const std::string& provider = ORCA_CLOUD_PROVIDER);
@@ -118,8 +118,9 @@ public:
int get_model_mall_detail_url(std::string* url, std::string id, const std::string& provider = ORCA_CLOUD_PROVIDER);
int get_my_profile(std::string token, unsigned int* http_code, std::string* http_body, const std::string& provider = ORCA_CLOUD_PROVIDER);
int get_my_token(std::string ticket, unsigned int* http_code, std::string* http_body, const std::string& provider = ORCA_CLOUD_PROVIDER);
int track_enable(bool enable, const std::string& provider = ORCA_CLOUD_PROVIDER);
int track_remove_files(const std::string& provider = ORCA_CLOUD_PROVIDER);
// Orca: telemetry only exists on the BBL cloud agent (Orca cloud has no track events).
int track_enable(bool enable);
int track_remove_files();
int track_event(std::string evt_key, std::string content, const std::string& provider = ORCA_CLOUD_PROVIDER);
int track_header(std::string header, const std::string& provider = ORCA_CLOUD_PROVIDER);
int track_update_property(std::string name, std::string value, std::string type = "string", const std::string& provider = ORCA_CLOUD_PROVIDER);
+10 -9
View File
@@ -56,6 +56,7 @@ constexpr const char* ORCA_DEFAULT_PUB_KEY = "sb_publishable_lvVe_whOi80SU9BPSxM
constexpr const char* ORCA_HEALTH_PATH = "/api/v1/health";
constexpr const char* ORCA_SYNC_PULL_PATH = "/api/v1/sync/pull";
constexpr const char* ORCA_SYNC_PUSH_PATH = "/api/v1/sync/push";
constexpr const char* ORCA_SYNC_FORCE_PUSH_PATH = "/api/v1/sync/force-push";
constexpr const char* ORCA_SYNC_DELETE_PATH = "/api/v1/sync/delete";
constexpr const char* ORCA_PROFILES_PATH = "/api/v1/sync/profiles";
constexpr const char* ORCA_SUBSCRIPTIONS_PATH = "/api/v1/subscriptions";
@@ -965,7 +966,7 @@ std::string OrcaCloudServiceAgent::request_setting_id(std::string name, std::map
return "";
}
int OrcaCloudServiceAgent::put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code)
int OrcaCloudServiceAgent::put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, bool force)
{
// Extract original_updated_time for Optimistic Concurrency Control
// If present, server will verify version before update. If absent, treated as insert.
@@ -989,7 +990,7 @@ int OrcaCloudServiceAgent::put_setting(std::string setting_id, std::string name,
}
}
auto result = sync_push(setting_id, name, content, original_updated_time);
auto result = sync_push(setting_id, name, content, original_updated_time, force);
if (http_code) *http_code = result.http_code;
if (result.success) {
@@ -1208,11 +1209,11 @@ int OrcaCloudServiceAgent::sync_pull(
}
}
SyncPushResult OrcaCloudServiceAgent::sync_push(
const std::string& profile_id,
const std::string& name,
const nlohmann::json& content,
const std::string& original_updated_time)
SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id,
const std::string& name,
const nlohmann::json& content,
const std::string& original_updated_time,
bool force)
{
SyncPushResult result;
result.success = false;
@@ -1243,7 +1244,7 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(
std::string response;
unsigned int http_code = 0;
int http_result = http_post(ORCA_SYNC_PUSH_PATH, body_str, &response, &http_code);
int http_result = http_post(force ? ORCA_SYNC_FORCE_PUSH_PATH : ORCA_SYNC_PUSH_PATH, body_str, &response, &http_code);
result.http_code = http_code;
@@ -1888,7 +1889,7 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string&
.on_error([&](std::string resp_body, std::string error, unsigned resp_status) {
result.success = false;
result.status = resp_status == 0 ? 404 : resp_status;
result.body = body;
result.body = resp_body;
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error;
})
.timeout_max(30)
+6 -8
View File
@@ -176,7 +176,12 @@ public:
// ========================================================================
int get_user_presets(std::map<std::string, std::map<std::string, std::string>>* user_presets) override;
std::string request_setting_id(std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code) override;
int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code) override;
int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, bool force = false) override;
SyncPushResult sync_push(const std::string& profile_id,
const std::string& name,
const nlohmann::json& content,
const std::string& original_updated_time = "",
bool force = false);
int get_setting_list(std::string bundle_version, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) override;
int get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) override;
int delete_setting(std::string setting_id) override;
@@ -294,13 +299,6 @@ private:
std::function<void(int http_code, const std::string& error)> on_error
);
SyncPushResult sync_push(
const std::string& profile_id,
const std::string& name,
const nlohmann::json& content,
const std::string& original_updated_time = ""
);
// HTTP request helpers
int http_get(const std::string& path, std::string* response_body, unsigned int* http_code);
int http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);
+4
View File
@@ -27,6 +27,8 @@
#include "Flashforge.hpp"
#include "SimplyPrint.hpp"
#include "ElegooLink.hpp"
#include "3DPrinterOS.hpp"
#include "Moonraker.hpp"
namespace fs = boost::filesystem;
using boost::optional;
@@ -67,6 +69,8 @@ PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config)
case htFlashforge: return new Flashforge(config);
case htSimplyPrint: return new SimplyPrint(config);
case htElegooLink: return new ElegooLink(config);
case ht3DPrinterOS: return new C3DPrinterOS(config);
case htMoonraker: return new Moonraker(config);
default: return nullptr;
}
} else {