mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-19 23:12:35 +00:00
Merge branch 'main' into fix/plugin-install-state
This commit is contained in:
+20
-3
@@ -24,6 +24,8 @@
|
||||
#include <iostream>
|
||||
#include <math.h>
|
||||
#include <csignal>
|
||||
#include <atomic>
|
||||
#include <new>
|
||||
|
||||
#if defined(__linux__) || defined(__LINUX__)
|
||||
#include <condition_variable>
|
||||
@@ -7608,6 +7610,9 @@ LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
|
||||
}*/
|
||||
|
||||
#if defined(_MSC_VER) || defined(__MINGW32__)
|
||||
// Guards against a failed allocation inside the dump re-entering the new-handler.
|
||||
static std::atomic<bool> g_dump_in_progress{false};
|
||||
|
||||
extern "C" {
|
||||
__declspec(dllexport) int __stdcall orcaslicer_main(int argc, wchar_t **argv)
|
||||
{
|
||||
@@ -7626,10 +7631,22 @@ extern "C" {
|
||||
//AddVectoredExceptionHandler(1, CBaseException::UnhandledExceptionFilter);
|
||||
SET_DEFULTER_HANDLER();
|
||||
#endif
|
||||
// Dump before unwinding, while the stack still names what asked for the memory. Throwing
|
||||
// std::bad_alloc is standard-permitted here and is what reaches generic_exception_handle().
|
||||
std::set_new_handler([]() {
|
||||
int *a = nullptr;
|
||||
*a = 0;
|
||||
});
|
||||
if (!g_dump_in_progress.exchange(true)) {
|
||||
try {
|
||||
// A null EXCEPTION_POINTERS walks the calling thread as it stands.
|
||||
CBaseException base(GetCurrentProcess(), GetCurrentProcessId(), NULL, nullptr);
|
||||
base.ShowCallstack();
|
||||
} catch (...) {
|
||||
// A failed dump must not displace the std::bad_alloc owed to the caller.
|
||||
}
|
||||
// ObjParser recovers from std::bad_alloc, so let a later one dump again.
|
||||
g_dump_in_progress = false;
|
||||
}
|
||||
throw std::bad_alloc();
|
||||
});
|
||||
// Call the UTF8 main.
|
||||
return CLI().run(argc, argv_ptrs.data());
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ double calculate_infill_rotation_angle(const PrintObject* object,
|
||||
if (template_string.empty()) {
|
||||
return Geometry::deg2rad(fixed_infill_angle);
|
||||
}
|
||||
// Convert the id to an index. Layer::id() counts the raft layers, object->layers() does not.
|
||||
const size_t first_object_layer_id = object->get_layer(0)->id();
|
||||
layer_id = layer_id > first_object_layer_id ? layer_id - first_object_layer_id : 0;
|
||||
double angle = 0.0;
|
||||
ConfigOptionFloats rotate_angles;
|
||||
const std::string search_string = "/NnZz$LlUuQq~^|#";
|
||||
@@ -75,6 +78,8 @@ double calculate_infill_rotation_angle(const PrintObject* object,
|
||||
double angle_start = 0;
|
||||
double limit_fill_z = object->get_layer(0)->bottom_z();
|
||||
double start_fill_z = limit_fill_z;
|
||||
// The raft height, or 0 without a raft.
|
||||
const double print_z_offset = object->slicing_parameters().object_print_z_min;
|
||||
bool _noop = false;
|
||||
auto fill_form = std::string::npos;
|
||||
bool _absolute = false;
|
||||
@@ -84,7 +89,8 @@ double calculate_infill_rotation_angle(const PrintObject* object,
|
||||
for (int i = 0; i <= layer_id; i++) {
|
||||
double fill_z = object->get_layer(i)->bottom_z();
|
||||
|
||||
if (limit_fill_z < object->get_layer(i)->slice_z) {
|
||||
// slice_z is measured from the bottom of the model, limit_fill_z from the build plate.
|
||||
if (limit_fill_z < object->get_layer(i)->slice_z + print_z_offset) {
|
||||
if (repeats) { // if repeats >0 then restore parameters for new iteration
|
||||
limit_fill_z += limit_fill_z - start_fill_z;
|
||||
start_fill_z = fill_z;
|
||||
|
||||
@@ -730,6 +730,42 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
return temp_set_by_gcode;
|
||||
}
|
||||
|
||||
struct CustomGCodeMotionStateChanges
|
||||
{
|
||||
bool acceleration = false;
|
||||
bool jerk = false;
|
||||
};
|
||||
|
||||
static bool custom_gcode_line_has_xy_parameter(const std::string &raw)
|
||||
{
|
||||
const size_t comment_pos = raw.find(';');
|
||||
const std::string_view code(raw.data(), comment_pos == std::string::npos ? raw.size() : comment_pos);
|
||||
return code.find_first_of("XxYy") != std::string_view::npos;
|
||||
}
|
||||
|
||||
static CustomGCodeMotionStateChanges custom_gcode_motion_state_changes(const std::string &gcode)
|
||||
{
|
||||
CustomGCodeMotionStateChanges changes;
|
||||
GCodeReader parser;
|
||||
parser.parse_buffer(gcode, [&changes](GCodeReader &parser, const GCodeReader::GCodeLine &line) {
|
||||
const std::string_view cmd = line.cmd();
|
||||
if (boost::iequals(cmd, "M204") || boost::iequals(cmd, "M201") ||
|
||||
boost::iequals(cmd, "M202"))
|
||||
changes.acceleration = true;
|
||||
else if ((boost::iequals(cmd, "M205") || boost::iequals(cmd, "M207") || boost::iequals(cmd, "M566")) &&
|
||||
custom_gcode_line_has_xy_parameter(line.raw()))
|
||||
changes.jerk = true;
|
||||
else if (boost::iequals(cmd, "SET_VELOCITY_LIMIT")) {
|
||||
changes.acceleration |= boost::icontains(line.raw(), "ACCEL=");
|
||||
changes.jerk |= boost::icontains(line.raw(), "SQUARE_CORNER_VELOCITY=");
|
||||
}
|
||||
|
||||
if (changes.acceleration && changes.jerk)
|
||||
parser.quit_parsing();
|
||||
});
|
||||
return changes;
|
||||
}
|
||||
|
||||
// BBS
|
||||
// start_pos refers to the last position before the wipe_tower.
|
||||
// end_pos refers to the wipe tower's start_pos.
|
||||
@@ -4335,6 +4371,11 @@ PlaceholderParserIntegration &ppi = m_placeholder_parser_integration;
|
||||
ppi.update_from_gcodewriter(m_writer);
|
||||
std::string output = ppi.parser.process(templ, current_filament_id, config_override, &ppi.output_config, &ppi.context);
|
||||
ppi.validate_output_vector_variables();
|
||||
const CustomGCodeMotionStateChanges motion_state_changes = custom_gcode_motion_state_changes(output);
|
||||
if (motion_state_changes.acceleration)
|
||||
m_writer.invalidate_acceleration();
|
||||
if (motion_state_changes.jerk)
|
||||
m_writer.invalidate_jerk();
|
||||
|
||||
if (const std::vector<double> &pos = ppi.opt_position->values; ppi.position != pos) {
|
||||
// Update G-code writer.
|
||||
|
||||
@@ -614,6 +614,8 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
||||
float leftover = 0.0f;
|
||||
for (size_t i = additional_buffer_idx; i < additional_buffer.size(); ++i)
|
||||
leftover += additional_buffer[i].second;
|
||||
BOOST_LOG_TRIVIAL(debug) << "calculate_time(is_final): leftover=" << leftover
|
||||
<< "s from " << (additional_buffer.size() - additional_buffer_idx) << " items";
|
||||
time += double(leftover);
|
||||
gcode_time.cache += leftover;
|
||||
} else {
|
||||
@@ -3492,6 +3494,7 @@ void GCodeProcessor::reset()
|
||||
m_extruder_blocks.clear();
|
||||
m_machine_start_gcode_end_line_id = (unsigned int) (-1);
|
||||
m_machine_end_gcode_start_line_id = (unsigned int) (-1);
|
||||
m_skip_end_gcode_delays = false;
|
||||
m_remaining_volume = std::vector<float>(MAXIMUM_EXTRUDER_NUMBER, 0.f);
|
||||
|
||||
m_line_id = 0;
|
||||
@@ -4219,6 +4222,14 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
|
||||
return;
|
||||
}
|
||||
|
||||
// End gcode marker: skip post-print M400 S/P dwells after this point so the M73 estimate reports
|
||||
// print-completion time, not post-print filtration/cooldown. BBS drops the same remainder in
|
||||
// calculate_time(is_final).
|
||||
if (comment == Machine_End_GCode_Start_Tag) {
|
||||
m_skip_end_gcode_delays = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Orca: Integrate filament consumption for purging performed to an external device and controlled via macros
|
||||
// (eg. Happy Hare) in the filament consumption stats.
|
||||
if (boost::starts_with(comment, GCodeProcessor::External_Purge_Tag)) {
|
||||
@@ -6401,6 +6412,10 @@ void GCodeProcessor::process_M400(const GCodeReader::GCodeLine& line)
|
||||
float value_p = 0.0;
|
||||
if (line.has_value('S', value_s) || line.has_value('P', value_p)) {
|
||||
value_s += value_p * 0.001;
|
||||
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
|
||||
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
|
||||
if (m_skip_end_gcode_delays)
|
||||
return;
|
||||
simulate_st_synchronize(value_s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,6 +1109,10 @@ class Print;
|
||||
std::vector<ExtruderPreHeating::ExtruderUsageBlcok> m_extruder_blocks;
|
||||
unsigned int m_machine_start_gcode_end_line_id{ (unsigned int) (-1) };
|
||||
unsigned int m_machine_end_gcode_start_line_id{ (unsigned int) (-1) };
|
||||
// Set when the MACHINE_END_GCODE_START tag is seen during the streaming parse; tells
|
||||
// process_M400 to skip post-print end-gcode dwells (air purification, timelapse, sound)
|
||||
// so they don't inflate the M73 estimate. BBS excludes them in calculate_time(is_final).
|
||||
bool m_skip_end_gcode_delays{ false };
|
||||
// Tracks, during the stream, which filament sits in each physical nozzle and which nozzle each
|
||||
// extruder currently carries. Written by both branches of the two-arg process_filament_change
|
||||
// (the fallback branch does occupancy bookkeeping only); read by the richer change-time model
|
||||
|
||||
@@ -1091,6 +1091,16 @@ namespace Slic3r
|
||||
if (layer + 1 < layer_filaments.size()) next_lf = layer_filaments[layer + 1];
|
||||
std::vector<unsigned int> filament_used_next_layer = collect_filaments_in_groups<unsigned int>(filament_sets, next_lf);
|
||||
|
||||
// Enable inter-layer forecast: when choosing filament ordering for current layer,
|
||||
// also consider next layer's filament set to minimize inter-layer transition flush.
|
||||
// solve_extruder_order_with_forcast() tries all permutations of curr+next layer
|
||||
// and picks the ordering that minimizes total flush across both layers.
|
||||
// This avoids expensive inter-layer transitions (e.g. ending layer with F2 when
|
||||
// next layer starts with F3, costing flush[F2→F3], instead of ending with F3
|
||||
// which gives flush[F3→F3]=0). Limited to ≤5 filaments due to O(N!×M!) complexity.
|
||||
// The per-nozzle base reorder does not use the inter-layer forecast. This function drives
|
||||
// BBL multi-extruder grouping cost and H2C ordering, so keeping it false avoids perturbing
|
||||
// existing H2D/H2C output.
|
||||
bool use_forcast = false;
|
||||
float tmp_cost = 0;
|
||||
std::vector<unsigned int> sequence;
|
||||
|
||||
@@ -1609,6 +1609,64 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config)
|
||||
if (max_vol_speed!= 0.f)
|
||||
m_filpar[idx].max_e_speed = (max_vol_speed / filament_area());
|
||||
|
||||
// Vortek H2C: carousel-specific ramming, precool, and reverse travel parameters
|
||||
{
|
||||
// Ramming speed: .first = extruder change, .second = nozzle change (carousel)
|
||||
// Use the dedicated ramming volumetric speed, falling back to max_vol_speed only when
|
||||
// the setting is nil/-1.
|
||||
float ramming_vol_speed = float(config.filament_ramming_volumetric_speed.get_at(idx));
|
||||
if (config.filament_ramming_volumetric_speed.is_nil(idx) || is_approx(config.filament_ramming_volumetric_speed.get_at(idx), -1.))
|
||||
ramming_vol_speed = max_vol_speed;
|
||||
m_filpar[idx].max_e_ramming_speed.first = (ramming_vol_speed / filament_area());
|
||||
|
||||
float ramming_vol_speed_nc = float(config.filament_ramming_volumetric_speed_nc.get_at(idx));
|
||||
if (config.filament_ramming_volumetric_speed_nc.is_nil(idx) || is_approx(config.filament_ramming_volumetric_speed_nc.get_at(idx), -1.))
|
||||
ramming_vol_speed_nc = max_vol_speed;
|
||||
m_filpar[idx].max_e_ramming_speed.second = (ramming_vol_speed_nc / filament_area());
|
||||
}
|
||||
{
|
||||
// Precool target temp: .first = extruder change, .second = nozzle change (carousel)
|
||||
// Precool is only active when enable_pre_heating is on; otherwise no precool temp/timing is
|
||||
// applied and the downstream precool_t stays 0, matching printers with pre-heating disabled.
|
||||
m_filpar[idx].precool_target_temp = {0, 0};
|
||||
if (config.enable_pre_heating.value) {
|
||||
if (!config.filament_pre_cooling_temperature.is_nil(idx) && config.filament_pre_cooling_temperature.get_at(idx) != 0)
|
||||
m_filpar[idx].precool_target_temp.first = config.filament_pre_cooling_temperature.get_at(idx);
|
||||
if (!config.filament_pre_cooling_temperature_nc.is_nil(idx) && config.filament_pre_cooling_temperature_nc.get_at(idx) != 0)
|
||||
m_filpar[idx].precool_target_temp.second = config.filament_pre_cooling_temperature_nc.get_at(idx);
|
||||
}
|
||||
}
|
||||
{
|
||||
// Precool timing: (nozzle_temp - precool_temp) / hotend_cooling_rate
|
||||
int extruder_count = m_is_multi_extruder ? 2 : 1; // H2C = 2 extruders
|
||||
float nozzle_temp = float(config.nozzle_temperature.is_nil(idx) ? 0 : config.nozzle_temperature.get_at(idx));
|
||||
float nozzle_temp_fl = float(config.nozzle_temperature_initial_layer.is_nil(idx) ? nozzle_temp : config.nozzle_temperature_initial_layer.get_at(idx));
|
||||
m_filpar[idx].precool_t.first.resize(extruder_count, 0.f);
|
||||
m_filpar[idx].precool_t.second.resize(extruder_count, 0.f);
|
||||
m_filpar[idx].precool_t_first_layer.first.resize(extruder_count, 0.f);
|
||||
m_filpar[idx].precool_t_first_layer.second.resize(extruder_count, 0.f);
|
||||
std::vector<double> cooling_rates = config.hotend_cooling_rate.values;
|
||||
for (int i = 0; i < extruder_count && i < (int)cooling_rates.size(); i++) {
|
||||
if (cooling_rates[i] < EPSILON) continue;
|
||||
if (m_filpar[idx].precool_target_temp.first != 0) {
|
||||
m_filpar[idx].precool_t.first[i] = std::max(0.f, nozzle_temp - float(m_filpar[idx].precool_target_temp.first)) / float(cooling_rates[i]);
|
||||
m_filpar[idx].precool_t_first_layer.first[i] = std::max(0.f, nozzle_temp_fl - float(m_filpar[idx].precool_target_temp.first)) / float(cooling_rates[i]);
|
||||
}
|
||||
if (m_filpar[idx].precool_target_temp.second != 0) {
|
||||
m_filpar[idx].precool_t.second[i] = std::max(0.f, nozzle_temp - float(m_filpar[idx].precool_target_temp.second)) / float(cooling_rates[i]);
|
||||
m_filpar[idx].precool_t_first_layer.second[i] = std::max(0.f, nozzle_temp_fl - float(m_filpar[idx].precool_target_temp.second)) / float(cooling_rates[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
// Ramming travel time: .first = extruder change, .second = nozzle change (carousel)
|
||||
m_filpar[idx].ramming_travel_time = {0.f, 0.f};
|
||||
if (!config.filament_ramming_travel_time.is_nil(idx))
|
||||
m_filpar[idx].ramming_travel_time.first = float(config.filament_ramming_travel_time.get_at(idx));
|
||||
if (!config.filament_ramming_travel_time_nc.is_nil(idx))
|
||||
m_filpar[idx].ramming_travel_time.second = float(config.filament_ramming_travel_time_nc.get_at(idx));
|
||||
}
|
||||
|
||||
m_perimeter_width = nozzle_diameter * Width_To_Nozzle_Ratio; // all extruders are now assumed to have the same diameter
|
||||
m_nozzle_change_perimeter_width = 2*m_perimeter_width;
|
||||
// BBS: remove useless config
|
||||
@@ -1893,7 +1951,7 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change(int old_filament_id, int
|
||||
.set_initial_tool(m_current_tool)
|
||||
.set_extrusion_flow(m_extrusion_flow)
|
||||
.set_y_shift(m_y_shift + (new_filament_id != (unsigned int) (-1) && (m_current_shape == SHAPE_REVERSED) ? m_layer_info->depth - m_layer_info->toolchanges_depth() : 0.f))
|
||||
.append("; Nozzle change start\n");
|
||||
.append(format_nozzle_change_tag(true, old_filament_id, new_filament_id));
|
||||
|
||||
box_coordinates cleaning_box(Vec2f(m_perimeter_width, m_perimeter_width), m_wipe_tower_width - 2 * m_perimeter_width,
|
||||
(new_filament_id != (unsigned int) (-1) ? wipe_depth + m_depth_traversed - m_perimeter_width : m_wipe_tower_depth - m_perimeter_width));
|
||||
@@ -1969,7 +2027,7 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change(int old_filament_id, int
|
||||
}
|
||||
}
|
||||
|
||||
writer.append("; Nozzle change end\n");
|
||||
writer.append(format_nozzle_change_tag(false, old_filament_id, new_filament_id));
|
||||
|
||||
result.start_pos = writer.start_pos_rotated();
|
||||
result.end_pos = writer.pos();
|
||||
@@ -2546,6 +2604,19 @@ void WipeTower::plan_toolchange(float z_par, float layer_height_par, unsigned in
|
||||
nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width;
|
||||
depth += nozzle_change_depth;
|
||||
}
|
||||
if (nozzle_change_depth == 0
|
||||
&& !m_filament_nozzle_map.empty()
|
||||
&& old_tool < m_filament_nozzle_map.size() && new_tool < m_filament_nozzle_map.size()
|
||||
&& m_filament_nozzle_map[old_tool] != m_filament_nozzle_map[new_tool]) {
|
||||
double e_flow = nozzle_change_extrusion_flow(layer_height_par);
|
||||
double length = m_filaments_change_length[old_tool] / e_flow;
|
||||
int nozzle_change_line_count = length / (m_wipe_tower_width - 2*m_nozzle_change_perimeter_width) + 1;
|
||||
if (has_tpu_filament())
|
||||
nozzle_change_depth = m_tpu_fixed_spacing * nozzle_change_line_count * m_nozzle_change_perimeter_width;
|
||||
else
|
||||
nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width;
|
||||
depth += nozzle_change_depth;
|
||||
}
|
||||
WipeTowerInfo::ToolChange tool_change = WipeTowerInfo::ToolChange(old_tool, new_tool, depth, 0.f, 0.f, wipe_volume, length_to_extrude, purge_volume);
|
||||
tool_change.nozzle_change_depth = nozzle_change_depth;
|
||||
m_plan.back().tool_changes.push_back(tool_change);
|
||||
@@ -2695,6 +2766,13 @@ bool WipeTower::is_petg_filament(int filament_id) const
|
||||
return m_filpar[filament_id].material == "PETG";
|
||||
}
|
||||
|
||||
bool WipeTower::is_need_reverse_travel(int filament_id, bool extruder_change) const
|
||||
{
|
||||
if (extruder_change)
|
||||
return m_filpar[filament_id].ramming_travel_time.first > EPSILON;
|
||||
return m_filpar[filament_id].ramming_travel_time.second > EPSILON;
|
||||
}
|
||||
|
||||
// BBS: consider both soluable and support properties
|
||||
// Return index of first toolchange that switches to non-soluble and non-support extruder
|
||||
// ot -1 if there is no such toolchange.
|
||||
@@ -2819,6 +2897,13 @@ WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool sol
|
||||
&& is_valid_last_layer(m_current_tool, m_cur_layer_id, m_z_pos)) {
|
||||
m_nozzle_change_result = nozzle_change_new(m_current_tool, new_tool, solid_nozzlechange);
|
||||
}
|
||||
if (m_nozzle_change_result.gcode.empty()
|
||||
&& !m_filament_nozzle_map.empty()
|
||||
&& m_current_tool < m_filament_nozzle_map.size() && new_tool < m_filament_nozzle_map.size()
|
||||
&& m_filament_nozzle_map[m_current_tool] != m_filament_nozzle_map[new_tool]
|
||||
&& is_valid_last_layer(m_current_tool, m_cur_layer_id, m_z_pos)) {
|
||||
m_nozzle_change_result = nozzle_change_new(m_current_tool, new_tool, solid_nozzlechange);
|
||||
}
|
||||
|
||||
size_t old_tool = m_current_tool;
|
||||
float wipe_depth = 0.f;
|
||||
@@ -2983,20 +3068,40 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change_new(int old_filament_id,
|
||||
}
|
||||
|
||||
float nz_extrusion_flow = nozzle_change_extrusion_flow(m_layer_height);
|
||||
float nozzle_change_speed = 60.0f * m_filpar[m_current_tool].max_e_speed / nz_extrusion_flow;
|
||||
nozzle_change_speed = solid_infill ? 40.f * 60.f : nozzle_change_speed;//If the contact layers belong to different categories, then reduce the speed.
|
||||
bool extruder_change = !is_in_same_extruder(old_filament_id, new_filament_id);
|
||||
float max_e_ramming = extruder_change
|
||||
? m_filpar[m_current_tool].max_e_ramming_speed.first
|
||||
: m_filpar[m_current_tool].max_e_ramming_speed.second;
|
||||
if (max_e_ramming < EPSILON) max_e_ramming = m_filpar[m_current_tool].max_e_speed; // fallback
|
||||
float nozzle_change_speed = 60.0f * max_e_ramming / nz_extrusion_flow;
|
||||
nozzle_change_speed = solid_infill ? 40.f * 60.f : nozzle_change_speed;
|
||||
|
||||
if (is_tpu_filament(m_current_tool)) {
|
||||
nozzle_change_speed *= 0.25;
|
||||
}
|
||||
float bridge_speed = std::min(60.0f * m_filpar[m_current_tool].max_e_speed / nozzle_change_extrusion_flow(0.2), nozzle_change_speed); // limit the bridge speed by add flow
|
||||
float bridge_speed = std::min(60.0f * max_e_ramming / nozzle_change_extrusion_flow(0.2), nozzle_change_speed);
|
||||
|
||||
WipeTowerWriter writer(m_layer_height, m_nozzle_change_perimeter_width, m_gcode_flavor, m_filpar);
|
||||
writer.set_extrusion_flow(nz_extrusion_flow)
|
||||
.set_z(m_z_pos)
|
||||
.set_initial_tool(m_current_tool)
|
||||
.set_y_shift(m_y_shift + (new_filament_id != (unsigned int) (-1) && (m_current_shape == SHAPE_REVERSED) ? m_layer_info->depth - m_layer_info->toolchanges_depth() : 0.f))
|
||||
.append("; Nozzle change start\n");
|
||||
.append(format_nozzle_change_tag(true, old_filament_id, new_filament_id));
|
||||
|
||||
if (!extruder_change && m_is_multiple_nozzle) {
|
||||
writer.append("M632 S" + std::to_string(new_filament_id) + " M N\n");
|
||||
// Use m_physical_extruder_map for heater index (matches format_line_M104 in add_M104_by_requirement)
|
||||
if (m_filpar[m_current_tool].precool_target_temp.second != 0) {
|
||||
int logical_ext = m_filament_map.empty() ? 0 : m_filament_map[m_current_tool] - 1;
|
||||
int phys_ext = (logical_ext >= 0 && logical_ext < (int)m_physical_extruder_map.size())
|
||||
? m_physical_extruder_map[logical_ext] : logical_ext;
|
||||
writer.append("M400\n");
|
||||
writer.append("M104 T" + std::to_string(phys_ext) + " S" +
|
||||
std::to_string(m_filpar[m_current_tool].precool_target_temp.second) + " N0\n");
|
||||
writer.append("M106 S255\n");
|
||||
}
|
||||
writer.append("M633\n");
|
||||
}
|
||||
|
||||
WipeTowerBlock* block = get_block_by_category(m_filpar[old_filament_id].category, false);
|
||||
if (!block) {
|
||||
@@ -3021,6 +3126,23 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change_new(int old_filament_id,
|
||||
dy = solid_infill ? m_nozzle_change_perimeter_width : dy;
|
||||
nozzle_change_line_count = solid_infill ? std::numeric_limits<int>::max() : nozzle_change_line_count;
|
||||
m_left_to_right = true;
|
||||
|
||||
if (extruder_change) {
|
||||
float ramming_length = nozzle_change_line_count * (xr - xl);
|
||||
int extruder_id = m_filament_map.empty() ? 0 : m_filament_map[m_current_tool] - 1;
|
||||
float precool_t = (extruder_id >= 0 && extruder_id < (int)m_filpar[m_current_tool].precool_t.first.size())
|
||||
? m_filpar[m_current_tool].precool_t.first[extruder_id] : 0.f;
|
||||
float precool_t_fl = (extruder_id >= 0 && extruder_id < (int)m_filpar[m_current_tool].precool_t_first_layer.first.size())
|
||||
? m_filpar[m_current_tool].precool_t_first_layer.first[extruder_id] : 0.f;
|
||||
float per_cooling_max_speed = nozzle_change_speed;
|
||||
if (is_first_layer() && precool_t_fl > EPSILON)
|
||||
per_cooling_max_speed = ramming_length / precool_t_fl * 60.f;
|
||||
else if (precool_t > EPSILON)
|
||||
per_cooling_max_speed = ramming_length / precool_t * 60.f;
|
||||
if (nozzle_change_speed > per_cooling_max_speed) nozzle_change_speed = per_cooling_max_speed;
|
||||
if (bridge_speed > per_cooling_max_speed) bridge_speed = per_cooling_max_speed;
|
||||
}
|
||||
|
||||
int real_nozzle_change_line_count = 0;
|
||||
bool need_change_flow = false;
|
||||
for (int i = 0; true; ++i) {
|
||||
@@ -3053,9 +3175,40 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change_new(int old_filament_id,
|
||||
block->last_nozzle_change_id = old_filament_id;
|
||||
|
||||
NozzleChangeResult result;
|
||||
if (is_tpu_filament(m_current_tool)) {
|
||||
if (!extruder_change && m_is_multiple_nozzle) {
|
||||
writer.append("M632 S" + std::to_string(new_filament_id) + " M N\n");
|
||||
}
|
||||
|
||||
if (is_need_reverse_travel(m_current_tool, extruder_change)) {
|
||||
bool left_to_right = !m_left_to_right;
|
||||
int tpu_line_count = (real_nozzle_change_line_count + 2 - 1) / 2; // nozzle_change_line_count / 2 round up
|
||||
int tpu_line_count = real_nozzle_change_line_count;
|
||||
float reverse_speed = nozzle_change_speed * 2; // reverse travel runs at double the nozzle-change speed
|
||||
float rt_time = extruder_change ? m_filpar[m_current_tool].ramming_travel_time.first
|
||||
: m_filpar[m_current_tool].ramming_travel_time.second;
|
||||
float need_reverse_travel_dis = rt_time * reverse_speed / 60.f;
|
||||
float real_travel_dis = tpu_line_count * (xr - xl - 2 * m_perimeter_width);
|
||||
if (real_travel_dis < need_reverse_travel_dis)
|
||||
reverse_speed *= real_travel_dis / need_reverse_travel_dis;
|
||||
writer.travel(writer.x(), writer.y() + dy/2);
|
||||
|
||||
for (int i = 0; true; ++i) {
|
||||
need_reverse_travel_dis -= (xr - xl - 2 * m_perimeter_width);
|
||||
float offset_dis = 0.f;
|
||||
if (need_reverse_travel_dis < 0)
|
||||
offset_dis = -need_reverse_travel_dis;
|
||||
if (left_to_right)
|
||||
writer.travel(xr - m_perimeter_width - offset_dis, writer.y(), reverse_speed);
|
||||
else
|
||||
writer.travel(xl + m_perimeter_width + offset_dis, writer.y(), reverse_speed);
|
||||
if (need_reverse_travel_dis < EPSILON) break;
|
||||
if (i == tpu_line_count - 1)
|
||||
break;
|
||||
writer.travel(writer.x(), writer.y() - dy);
|
||||
left_to_right = !left_to_right;
|
||||
}
|
||||
} else if (is_tpu_filament(m_current_tool)) {
|
||||
bool left_to_right = !m_left_to_right;
|
||||
int tpu_line_count = (real_nozzle_change_line_count + 2 - 1) / 2;
|
||||
nozzle_change_speed *= 2;
|
||||
writer.travel(writer.x(), writer.y() - m_nozzle_change_perimeter_width);
|
||||
|
||||
@@ -3080,12 +3233,15 @@ WipeTower::NozzleChangeResult WipeTower::nozzle_change_new(int old_filament_id,
|
||||
}
|
||||
}
|
||||
|
||||
writer.append("; Nozzle change end\n");
|
||||
if (!extruder_change && m_is_multiple_nozzle) writer.append("M633\n");
|
||||
|
||||
writer.append(format_nozzle_change_tag(false, old_filament_id, new_filament_id));
|
||||
|
||||
result.start_pos = writer.start_pos_rotated();
|
||||
result.origin_start_pos = initial_position;
|
||||
result.end_pos = writer.pos_rotated();
|
||||
result.gcode = writer.gcode();
|
||||
result.is_extruder_change = extruder_change;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -3506,29 +3662,26 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat
|
||||
// Emit the arriving-hotend pre-heat inside the M632/M633 nozzle-change barrier. `M632 S<tool>[ H<nozzle>]
|
||||
// M N` opens the barrier (M = firmware nozzle-change flag, N = slicer generated), the M104 sets the
|
||||
// arriving hotend temp, and `M633` closes it. H2C's grouping is static (no dynamic nozzle map), so the
|
||||
// H<nozzle> field is omitted (a dynamic nozzle map would supply a real nozzle id, a static map -1 => no
|
||||
// H<nozzle> field is omitted (a dynamic nozzle map would supply a real nozzle id, a static map -1 =>no
|
||||
// H). The counterproductive fan-on (M106 S255) used for departing-tool cooldown is intentionally
|
||||
// omitted, since this is a pre-HEAT of the arriving tool. The whole helper is only ever called from
|
||||
// add_M104_by_requirement, which is gated on m_is_multiple_nozzle (extruder_max_nozzle_count>1) => H2C
|
||||
// only; every other printer's wipe tower is untouched. The M632 M-flag is itself a firmware barrier, so
|
||||
// a preceding M400 wait is subsumed.
|
||||
// only; every other printer's wipe tower is untouched.
|
||||
// BBS: extruder change preheat uses M400 + M104 WITHOUT M632/M633 barrier.
|
||||
// M632 barriers are only for carousel nozzle changes (emitted in nozzle_change_new/ramming).
|
||||
auto format_line_M104 = [this](int target_temp, int target_extruder = -1, bool wait_for_moves = true, const std::string &comment = "") {
|
||||
std::string buffer;
|
||||
buffer += "M632 S" + std::to_string(m_current_tool) + " M N\n";
|
||||
if (wait_for_moves)
|
||||
buffer += "M400\n";
|
||||
buffer += "M104";
|
||||
if (target_extruder != -1 && target_extruder < (int) m_physical_extruder_map.size())
|
||||
buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder]));
|
||||
buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by the slicer
|
||||
if (!comment.empty()) buffer += " ;" + comment;
|
||||
buffer += '\n';
|
||||
buffer += "M633\n";
|
||||
(void) wait_for_moves; // the M632 M-flag barrier replaces the former M400 wait
|
||||
return buffer;
|
||||
};
|
||||
// Suppress the pre-heat M104 on the first layer and on solid (contact) toolchanges (should_heating).
|
||||
// m_is_multiple_nozzle folds in the H2C gate so single-nozzle output is untouched.
|
||||
// Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (layer-static) because Orca's
|
||||
// wipe tower is extruder-level rather than tracking a per-layer nozzle map.
|
||||
// m_is_multiple_nozzle gate needed because Orca calls toolchange_wipe_new for ALL printers (BBS has it H2C-only).
|
||||
bool should_heating = m_is_multiple_nozzle && m_filpar[m_current_tool].filament_cooling_before_tower > EPSILON &&
|
||||
!solid_tool_toolchange && !is_first_layer();
|
||||
auto add_M104_by_requirement = [&writer, &format_line_M104, &should_heating, this]() {
|
||||
@@ -3710,6 +3863,18 @@ bool WipeTower::is_in_same_extruder(int filament_id_1, int filament_id_2)
|
||||
return m_filament_map[filament_id_1] == m_filament_map[filament_id_2];
|
||||
}
|
||||
|
||||
std::string WipeTower::format_nozzle_change_tag(bool start, int old_filament_id, int new_filament_id) const
|
||||
{
|
||||
const std::string &tag = start ? GCodeProcessor::Nozzle_Change_Start_Tag : GCodeProcessor::Nozzle_Change_End_Tag;
|
||||
int old_nozzle = (old_filament_id >= 0 && old_filament_id < (int)m_filament_nozzle_map.size())
|
||||
? m_filament_nozzle_map[old_filament_id] : -1;
|
||||
int new_nozzle = (new_filament_id >= 0 && new_filament_id < (int)m_filament_nozzle_map.size())
|
||||
? m_filament_nozzle_map[new_filament_id] : -1;
|
||||
char buff[96];
|
||||
snprintf(buff, sizeof(buff), ";%s OF%d NF%d ON%d NN%d\n", tag.c_str(), old_filament_id, new_filament_id, old_nozzle, new_nozzle);
|
||||
return std::string(buff);
|
||||
}
|
||||
|
||||
// Per-extruder printable-height clamp: is an extruder still allowed to print on this wipe-tower layer,
|
||||
// or is it its final layer above the extruder's printable height?
|
||||
// Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (1-based map, layer-static),
|
||||
@@ -3908,6 +4073,19 @@ void WipeTower::plan_tower_new()
|
||||
nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width;
|
||||
depth += nozzle_change_depth;
|
||||
}
|
||||
if (nozzle_change_depth == 0
|
||||
&& !m_filament_nozzle_map.empty()
|
||||
&& toolchange.old_tool < (int)m_filament_nozzle_map.size() && toolchange.new_tool < (int)m_filament_nozzle_map.size()
|
||||
&& m_filament_nozzle_map[toolchange.old_tool] != m_filament_nozzle_map[toolchange.new_tool]) {
|
||||
double e_flow = nozzle_change_extrusion_flow(m_plan[idx].height);
|
||||
double length = m_filaments_change_length[toolchange.old_tool] / e_flow;
|
||||
int nozzle_change_line_count = length / (m_wipe_tower_width - 2*m_nozzle_change_perimeter_width) + 1;
|
||||
if (has_tpu_filament())
|
||||
nozzle_change_depth = m_tpu_fixed_spacing * nozzle_change_line_count * m_nozzle_change_perimeter_width;
|
||||
else
|
||||
nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width;
|
||||
depth += nozzle_change_depth;
|
||||
}
|
||||
toolchange.nozzle_change_depth = nozzle_change_depth;
|
||||
toolchange.required_depth = depth;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ public:
|
||||
Vec2f origin_start_pos; // not rotated
|
||||
|
||||
std::vector<Vec2f> wipe_path;
|
||||
bool is_extruder_change{true};
|
||||
};
|
||||
|
||||
struct ToolChangeResult
|
||||
@@ -309,6 +310,8 @@ public:
|
||||
int get_number_of_toolchanges() const { return m_num_tool_changes; }
|
||||
|
||||
void set_filament_map(const std::vector<int> &filament_map) { m_filament_map = filament_map; }
|
||||
// Vortek H2C: filament_id → physical nozzle_id for carousel rotation detection
|
||||
void set_filament_nozzle_map(const std::vector<int> &nozzle_map) { m_filament_nozzle_map = nozzle_map; }
|
||||
|
||||
void set_has_tpu_filament(bool has_tpu) { m_has_tpu_filament = has_tpu; }
|
||||
bool has_tpu_filament() const { return m_has_tpu_filament; }
|
||||
@@ -356,6 +359,12 @@ public:
|
||||
// Distance (in mm of filament) that a hotend is allowed to pre-cool before the
|
||||
// tower is reached; drives the prime-tower heating-during-wipe model (multi-nozzle only).
|
||||
float filament_cooling_before_tower = 0.f;
|
||||
// .first = extruder change, .second = nozzle change (carousel)
|
||||
std::pair<float,float> max_e_ramming_speed{0.f, 0.f};
|
||||
std::pair<float,float> ramming_travel_time{0.f, 0.f};
|
||||
std::pair<int,int> precool_target_temp{0, 0};
|
||||
std::pair<std::vector<float>,std::vector<float>> precool_t;
|
||||
std::pair<std::vector<float>,std::vector<float>> precool_t_first_layer;
|
||||
};
|
||||
|
||||
|
||||
@@ -395,6 +404,8 @@ public:
|
||||
void add_depth_to_block(int filament_id, int filament_adhesiveness_category, float depth, bool is_nozzle_change = false);
|
||||
int get_filament_category(int filament_id);
|
||||
bool is_in_same_extruder(int filament_id_1, int filament_id_2);
|
||||
// Vortek H2C: format BBS-compatible NOZZLE_CHANGE_START/END tag with OF/NF/ON/NN payload
|
||||
std::string format_nozzle_change_tag(bool start, int old_filament_id, int new_filament_id) const;
|
||||
void reset_block_status();
|
||||
int get_wall_filament_for_all_layer();
|
||||
// for generate new wipe tower
|
||||
@@ -453,6 +464,7 @@ private:
|
||||
size_t m_cur_layer_id;
|
||||
NozzleChangeResult m_nozzle_change_result;
|
||||
std::vector<int> m_filament_map;
|
||||
std::vector<int> m_filament_nozzle_map; // Vortek H2C: filament_id → physical nozzle_id
|
||||
bool m_has_tpu_filament{false};
|
||||
bool m_is_multi_extruder{false};
|
||||
bool m_use_gap_wall{false};
|
||||
@@ -555,6 +567,7 @@ private:
|
||||
|
||||
bool is_tpu_filament(int filament_id) const;
|
||||
bool is_petg_filament(int filament_id) const;
|
||||
bool is_need_reverse_travel(int filament_id, bool extruder_change) const;
|
||||
|
||||
// BBS
|
||||
box_coordinates align_perimeter(const box_coordinates& perimeter_box);
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "CustomGCode.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Line.hpp"
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
@@ -99,9 +101,87 @@ void GCodeWriter::apply_print_config(const PrintConfig &print_config)
|
||||
m_max_jerk_z = LIMITS(machine_max_jerk_z);
|
||||
m_max_jerk_e = LIMITS(machine_max_jerk_e);
|
||||
m_resolution = print_config.resolution.value;
|
||||
|
||||
#undef LIMITS
|
||||
#undef LIMITS_UINT
|
||||
// Orca: capture the printable area(s) so a spiral lift can be skipped when its
|
||||
// circle would leave the boundary and collide with the print limits. Full polygons
|
||||
// are stored (not a bounding box) so the check stays correct for non-rectangular
|
||||
// beds, and per-extruder areas are kept so printers with different boundaries per
|
||||
// extruder use the right limit for whichever extruder is active.
|
||||
auto to_scaled_polygon = [](const Pointfs &pts) {
|
||||
Polygon poly;
|
||||
poly.points.reserve(pts.size());
|
||||
for (const Vec2d &p : pts)
|
||||
poly.points.emplace_back(coord_t(scale_(p.x())), coord_t(scale_(p.y())));
|
||||
poly.make_counter_clockwise();
|
||||
return poly;
|
||||
};
|
||||
|
||||
m_bed_printable_area.points.clear();
|
||||
m_extruder_printable_areas.clear();
|
||||
|
||||
if (print_config.printable_area.values.size() >= 3)
|
||||
m_bed_printable_area = to_scaled_polygon(print_config.printable_area.values);
|
||||
|
||||
const std::vector<Pointfs> &extruder_areas = print_config.extruder_printable_area.values;
|
||||
if (!extruder_areas.empty()) {
|
||||
m_extruder_printable_areas.resize(extruder_areas.size());
|
||||
for (size_t i = 0; i < extruder_areas.size(); ++i) {
|
||||
if (extruder_areas[i].size() < 3) {
|
||||
// No dedicated area for this extruder: it can reach the whole bed.
|
||||
m_extruder_printable_areas[i] = m_bed_printable_area;
|
||||
continue;
|
||||
}
|
||||
Polygon extruder_poly = to_scaled_polygon(extruder_areas[i]);
|
||||
if (m_bed_printable_area.points.size() < 3) {
|
||||
m_extruder_printable_areas[i] = std::move(extruder_poly);
|
||||
continue;
|
||||
}
|
||||
// The reachable area is the extruder area clipped to the bed. Bed shapes are
|
||||
// convex in practice, so keep the largest resulting contour.
|
||||
Polygons clipped = intersection(extruder_poly, m_bed_printable_area);
|
||||
const Polygon *largest = nullptr;
|
||||
double best_area = 0.;
|
||||
for (const Polygon &p : clipped) {
|
||||
double a = std::abs(p.area());
|
||||
if (a > best_area) { best_area = a; largest = &p; }
|
||||
}
|
||||
m_extruder_printable_areas[i] = largest ? *largest : std::move(extruder_poly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Polygon *GCodeWriter::active_printable_area() const
|
||||
{
|
||||
if (const Extruder *e = this->filament()) {
|
||||
size_t id = e->extruder_id();
|
||||
if (id < m_extruder_printable_areas.size() && m_extruder_printable_areas[id].points.size() >= 3)
|
||||
return &m_extruder_printable_areas[id];
|
||||
}
|
||||
if (m_bed_printable_area.points.size() >= 3)
|
||||
return &m_bed_printable_area;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool GCodeWriter::spiral_lift_fits_printable_area(const Vec2d ¢er, double radius) const
|
||||
{
|
||||
const Polygon *area = this->active_printable_area();
|
||||
if (area == nullptr)
|
||||
return true; // Boundary unknown: don't restrict (preserve previous behavior).
|
||||
|
||||
const Point c = Point::new_scale(center.x(), center.y());
|
||||
const double r_scaled = scale_(radius);
|
||||
const double r2 = r_scaled * r_scaled;
|
||||
|
||||
// The spiral traces a full circle of `radius` around `center`, so the center must lie
|
||||
// inside the printable area and every edge must be at least `radius` away from it.
|
||||
if (!area->contains(c))
|
||||
return false;
|
||||
const Points &pts = area->points;
|
||||
for (size_t i = 0, n = pts.size(); i < n; ++i)
|
||||
if (Line::distance_to_squared(c, pts[i], pts[(i + 1) % n]) < r2)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GCodeWriter::set_extruders(std::vector<unsigned int> extruder_ids)
|
||||
@@ -731,14 +811,19 @@ std::string GCodeWriter::eager_lift(const LiftType type) {
|
||||
}
|
||||
|
||||
// BBS: spiral lift only safe with known position
|
||||
// TODO: check the arc will move within bed area
|
||||
if (type == LiftType::SpiralLift && this->is_current_position_clear()) {
|
||||
double radius = target_lift / (2 * PI * atan(filament()->travel_slope()));
|
||||
// static spiral alignment when no move in x,y plane.
|
||||
// spiral centra is a radius distance to the right (y=0)
|
||||
// spiral centra is a radius distance to the right (y=0)
|
||||
Vec2d ij_offset = { radius, 0 };
|
||||
if (target_lift > 0) {
|
||||
// Orca: keep the spiral inside the active extruder's printable area, otherwise
|
||||
// fall back to a normal lift to avoid colliding with the print boundary. m_pos
|
||||
// includes the plate offset, so remove it to match the printable area coordinates.
|
||||
const Vec2d spiral_center = { m_pos.x() - m_x_offset + ij_offset.x(), m_pos.y() - m_y_offset + ij_offset.y() };
|
||||
if (target_lift > 0 && this->spiral_lift_fits_printable_area(spiral_center, radius)) {
|
||||
lift_move = this->_spiral_travel_to_z(m_pos(2) + target_lift, ij_offset, "spiral lift Z");
|
||||
} else if (target_lift > 0) {
|
||||
lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z");
|
||||
}
|
||||
}
|
||||
//BBS: if position is unknown use normal lift
|
||||
@@ -793,7 +878,15 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
|
||||
double radius = delta(2) / (2 * PI * atan(this->filament()->travel_slope()));
|
||||
Vec2d ij_offset = radius * delta_no_z.normalized();
|
||||
ij_offset = { -ij_offset(1), ij_offset(0) };
|
||||
slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z");
|
||||
// Orca: only perform the spiral lift if its full circle stays inside the
|
||||
// printable area of the active extruder, otherwise fall back to a normal
|
||||
// lift to avoid colliding with the print boundary. `source` is already in
|
||||
// bed coordinates (plate offset removed), matching the printable area.
|
||||
const Vec2d spiral_center = { source.x() + ij_offset.x(), source.y() + ij_offset.y() };
|
||||
if (this->spiral_lift_fits_printable_area(spiral_center, radius))
|
||||
slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z");
|
||||
else
|
||||
slop_move = _travel_to_z(target.z(), "normal lift Z");
|
||||
}
|
||||
//BBS: SlopeLift
|
||||
else if (m_to_lift_type == LiftType::SlopeLift &&
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <charconv>
|
||||
#include "Extruder.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "GCode/CoolingBuffer.hpp"
|
||||
|
||||
@@ -132,6 +133,8 @@ public:
|
||||
const bool is_bbl_printers() const {return m_is_bbl_printers;}
|
||||
void set_is_first_layer(bool bval) { m_is_first_layer = bval; }
|
||||
GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; }
|
||||
void invalidate_acceleration() { m_last_acceleration = 0; m_last_travel_acceleration = 0; }
|
||||
void invalidate_jerk() { m_last_jerk = 0; }
|
||||
|
||||
// Returns whether this flavor supports separate print and travel acceleration.
|
||||
static bool supports_separate_travel_acceleration(GCodeFlavor flavor);
|
||||
@@ -181,6 +184,14 @@ public:
|
||||
|
||||
// Orca: slicing resolution in mm
|
||||
double m_resolution = 0.01;
|
||||
// Orca: printable area polygons (scaled, bed coordinates) used to keep spiral lifts
|
||||
// from colliding with the print boundary. m_extruder_printable_areas holds the
|
||||
// per-extruder reachable area (intersected with the bed) when a printer defines
|
||||
// different boundaries per extruder; m_bed_printable_area is the global fallback.
|
||||
// Storing full polygons (rather than a bounding box) keeps the check correct for
|
||||
// non-rectangular beds such as delta/circular printers.
|
||||
Polygon m_bed_printable_area;
|
||||
std::vector<Polygon> m_extruder_printable_areas;
|
||||
|
||||
std::string m_gcode_label_objects_start;
|
||||
std::string m_gcode_label_objects_end;
|
||||
@@ -197,6 +208,10 @@ public:
|
||||
|
||||
std::string _travel_to_z(double z, const std::string &comment);
|
||||
std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment);
|
||||
// Orca: printable area of the active extruder (per-extruder when configured, otherwise the bed). Null when unknown.
|
||||
const Polygon *active_printable_area() const;
|
||||
// Orca: true if a full spiral-lift circle (center in bed coordinates, mm) fits inside the active printable area.
|
||||
bool spiral_lift_fits_printable_area(const Vec2d ¢er, double radius) const;
|
||||
std::string _retract(double length, double restart_extra, const std::string &comment);
|
||||
std::string set_acceleration_internal(Acceleration type, unsigned int acceleration);
|
||||
|
||||
|
||||
@@ -1177,6 +1177,18 @@ static bool is_volume_sinking(const indexed_triangle_set &its, const Transform3d
|
||||
|
||||
//#define MMU_SEGMENTATION_DEBUG_TOP_BOTTOM
|
||||
|
||||
double resolve_outer_wall_line_width(const PrintRegionConfig ®ion_config, const PrintObjectConfig &object_config, const PrintConfig &print_config)
|
||||
{
|
||||
// A filament id of 0 underflows, and get_at() then falls back to the first nozzle.
|
||||
const double nozzle_diameter = print_config.nozzle_diameter.get_at(region_config.outer_wall_filament_id - 1);
|
||||
ConfigOptionFloatOrPercent width = region_config.outer_wall_line_width;
|
||||
if (width.value == 0)
|
||||
width = object_config.line_width;
|
||||
if (!width.percent && width.value <= 0.)
|
||||
return Flow::auto_extrusion_width(frExternalPerimeter, float(nozzle_diameter));
|
||||
return width.get_abs_value(nozzle_diameter);
|
||||
}
|
||||
|
||||
// Returns segmentation of top and bottom layers based on painting in segmentation gizmos.
|
||||
static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_layers(const PrintObject &print_object,
|
||||
const std::vector<ExPolygons> &input_expolygons,
|
||||
@@ -1347,8 +1359,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
// As this region may split existing regions, we collect statistics over all regions for color_idx == 0.
|
||||
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);
|
||||
double outer_wall_line_width = resolve_outer_wall_line_width(config, print_object.config(), print_object.print()->config());
|
||||
out.extrusion_width = std::max<float>(out.extrusion_width, outer_wall_line_width);
|
||||
out.top_shell_layers = std::max<int>(out.top_shell_layers, config.top_shell_layers);
|
||||
out.bottom_shell_layers = std::max<int>(out.bottom_shell_layers, config.bottom_shell_layers);
|
||||
|
||||
@@ -9,6 +9,9 @@ namespace Slic3r {
|
||||
class ExPolygon;
|
||||
class ModelVolume;
|
||||
class PrintObject;
|
||||
class PrintConfig;
|
||||
class PrintObjectConfig;
|
||||
class PrintRegionConfig;
|
||||
class FacetsAnnotation;
|
||||
|
||||
using ExPolygons = std::vector<ExPolygon>;
|
||||
@@ -52,6 +55,9 @@ std::vector<std::vector<ExPolygons>> multi_material_segmentation_by_painting(con
|
||||
// Returns fuzzy skin segmentation based on painting in fuzzy skin segmentation gizmo
|
||||
std::vector<std::vector<ExPolygons>> fuzzy_skin_segmentation_by_painting(const PrintObject &print_object, const std::function<void()> &throw_on_cancel_callback);
|
||||
|
||||
// Effective outer-wall line width for a region, resolved against its own nozzle with PrintRegion::flow's fallback.
|
||||
double resolve_outer_wall_line_width(const PrintRegionConfig ®ion_config, const PrintObjectConfig &object_config, const PrintConfig &print_config);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
namespace boost::polygon {
|
||||
|
||||
@@ -1376,7 +1376,7 @@ static std::vector<std::string> s_Preset_filament_options {/*"filament_colour",
|
||||
"filament_ramming_travel_time", "filament_ramming_travel_time_nc",
|
||||
"filament_pre_cooling_temperature", "filament_pre_cooling_temperature_nc",
|
||||
"filament_preheat_temperature_delta", "filament_retract_length_nc",
|
||||
"filament_change_length_nc", "filament_prime_volume_nc",
|
||||
"filament_change_length_nc", "filament_prime_volume", "filament_prime_volume_nc",
|
||||
"long_retractions_when_ec", "retraction_distances_when_ec",
|
||||
"plugin_config_overrides",
|
||||
//ams chamber
|
||||
@@ -3504,7 +3504,7 @@ inline t_config_option_keys deep_diff(const ConfigBase &config_this, const Confi
|
||||
if (this_opt != nullptr && other_opt != nullptr && *this_opt != *other_opt)
|
||||
{
|
||||
//BBS: add bed_exclude_area
|
||||
if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "thumbnails" || opt_key == "wrapping_exclude_area") {
|
||||
if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "thumbnails" || opt_key == "wrapping_exclude_area" || opt_key == "slicing_pipeline_plugin") {
|
||||
// Scalar variable, or a vector variable, which is independent from number of extruders,
|
||||
// thus the vector is presented to the user as a single input.
|
||||
diff.emplace_back(opt_key);
|
||||
|
||||
+86
-19
@@ -3605,7 +3605,8 @@ std::vector<std::set<int>> Print::get_physical_unprintable_filaments(const std::
|
||||
return physical_unprintables;
|
||||
|
||||
auto get_unprintable_extruder_id = [&](unsigned int filament_idx) -> int {
|
||||
int status = m_config.filament_printable.values[filament_idx];
|
||||
// filament_printable may be shorter than the filament count; get_at() clamps.
|
||||
int status = m_config.filament_printable.get_at(filament_idx);
|
||||
for (int i = 0; i < extruder_num; ++i) {
|
||||
if (!(status >> i & 1)) {
|
||||
return i;
|
||||
@@ -4017,6 +4018,8 @@ void Print::_make_wipe_tower()
|
||||
m_wipe_tower_data.tool_ordering.empty() ? 0.f : m_wipe_tower_data.tool_ordering.back().print_z, m_wipe_tower_data.tool_ordering.all_extruders());
|
||||
wipe_tower.set_has_tpu_filament(this->has_tpu_filament());
|
||||
wipe_tower.set_filament_map(this->get_filament_maps());
|
||||
// Vortek H2C: pass nozzle-level map for carousel rotation detection in tool_change_new()
|
||||
wipe_tower.set_filament_nozzle_map(this->get_filament_nozzle_maps());
|
||||
// Feed the has_filament_switcher device flag (develop-only dynamic key, read defensively from
|
||||
// the full config — no shipping profile sets it) and the shared printable bed used by the PETG
|
||||
// pre-extrusion offset clamp. Both are inert unless has_filament_switcher is set.
|
||||
@@ -4052,14 +4055,32 @@ void Print::_make_wipe_tower()
|
||||
multi_extruder_flush.emplace_back(wipe_volumes);
|
||||
}
|
||||
|
||||
// Use NozzleStatusRecorder for per-carousel-slot tracking (BBS pattern).
|
||||
// The original Orca code tracked per-extruder (2 slots), which collapsed all
|
||||
// carousel filaments into one slot and caused massive redundant AMS flushing.
|
||||
auto group_result = get_layered_nozzle_group_result();
|
||||
MultiNozzleUtils::NozzleStatusRecorder nozzle_recorder;
|
||||
// Fallback (group_result == null) per-physical-nozzle tracking, matching the original
|
||||
// pre-port behavior: remembers the last filament loaded in each physical nozzle slot.
|
||||
std::vector<unsigned int> nozzle_cur_filament_ids(nozzle_nums, (unsigned int) -1);
|
||||
|
||||
std::vector<int>filament_maps = get_filament_maps();
|
||||
int layer_idx = -1;
|
||||
|
||||
std::vector<unsigned int> nozzle_cur_filament_ids(nozzle_nums, -1);
|
||||
unsigned int current_filament_id = m_wipe_tower_data.tool_ordering.first_extruder();
|
||||
size_t cur_nozzle_id = filament_maps[current_filament_id] - 1;
|
||||
nozzle_cur_filament_ids[cur_nozzle_id] = current_filament_id;
|
||||
// Initialize NozzleStatusRecorder with the first filament's carousel slot
|
||||
if (group_result) {
|
||||
auto nozzle = group_result->get_nozzle_for_filament(current_filament_id, layer_idx);
|
||||
if (nozzle)
|
||||
nozzle_recorder.set_nozzle_status(nozzle->group_id, current_filament_id, nozzle->extruder_id);
|
||||
} else {
|
||||
size_t cur_nozzle_id = filament_maps[current_filament_id] - 1;
|
||||
nozzle_cur_filament_ids[cur_nozzle_id] = current_filament_id;
|
||||
}
|
||||
|
||||
for (auto& layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { // for all layers
|
||||
++layer_idx;
|
||||
|
||||
if (!layer_tools.has_wipe_tower) continue;
|
||||
bool first_layer = &layer_tools == &m_wipe_tower_data.tool_ordering.front();
|
||||
wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, current_filament_id);
|
||||
@@ -4070,30 +4091,76 @@ void Print::_make_wipe_tower()
|
||||
if (filament_id == current_filament_id)
|
||||
continue;
|
||||
|
||||
int nozzle_id = filament_maps[filament_id] - 1;
|
||||
unsigned int pre_filament_id = nozzle_cur_filament_ids[nozzle_id];
|
||||
|
||||
float volume_to_purge = 0;
|
||||
if (pre_filament_id != (unsigned int)(-1) && pre_filament_id != filament_id) {
|
||||
volume_to_purge = multi_extruder_flush[nozzle_id][pre_filament_id][filament_id];
|
||||
// Fast purge mode uses flush_multiplier_fast; Default is inert.
|
||||
float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast) ? m_config.flush_multiplier_fast.get_at(nozzle_id)
|
||||
: m_config.flush_multiplier.get_at(nozzle_id);
|
||||
volume_to_purge *= flush_multiplier;
|
||||
volume_to_purge = pre_filament_id == -1 ? 0 :
|
||||
layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_filament_id, filament_id, volume_to_purge);
|
||||
|
||||
// Per-carousel-slot purge tracking via NozzleStatusRecorder
|
||||
if (group_result) {
|
||||
auto nozzle_info = group_result->get_nozzle_for_filament(filament_id, layer_idx);
|
||||
if (nozzle_info) {
|
||||
int extruder_id = nozzle_info->extruder_id;
|
||||
int nozzle_id = nozzle_info->group_id;
|
||||
int prev_nozzle_filament = nozzle_recorder.get_filament_in_nozzle(nozzle_id);
|
||||
|
||||
if (!nozzle_recorder.is_nozzle_empty(nozzle_id) &&
|
||||
static_cast<int>(filament_id) != prev_nozzle_filament) {
|
||||
volume_to_purge = multi_extruder_flush[extruder_id][prev_nozzle_filament][filament_id];
|
||||
// Fast purge mode uses flush_multiplier_fast; Default is inert.
|
||||
float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast)
|
||||
? m_config.flush_multiplier_fast.get_at(extruder_id)
|
||||
: m_config.flush_multiplier.get_at(extruder_id);
|
||||
volume_to_purge *= flush_multiplier;
|
||||
volume_to_purge = layer_tools.wiping_extrusions().mark_wiping_extrusions(
|
||||
*this, current_filament_id, filament_id, volume_to_purge);
|
||||
}
|
||||
nozzle_recorder.set_nozzle_status(nozzle_id, filament_id, extruder_id);
|
||||
}
|
||||
} else {
|
||||
// Fallback: original Orca per-physical-nozzle path (non-carousel printers).
|
||||
// Flush source is the last filament that occupied THIS nozzle, guarded so the
|
||||
// first use of a nozzle incurs no flush.
|
||||
int nozzle_id = filament_maps[filament_id] - 1;
|
||||
unsigned int pre_filament_id = nozzle_cur_filament_ids[nozzle_id];
|
||||
if (pre_filament_id != (unsigned int) -1 && pre_filament_id != filament_id) {
|
||||
volume_to_purge = multi_extruder_flush[nozzle_id][pre_filament_id][filament_id];
|
||||
float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast)
|
||||
? m_config.flush_multiplier_fast.get_at(nozzle_id)
|
||||
: m_config.flush_multiplier.get_at(nozzle_id);
|
||||
volume_to_purge *= flush_multiplier;
|
||||
volume_to_purge = layer_tools.wiping_extrusions().mark_wiping_extrusions(
|
||||
*this, current_filament_id, filament_id, volume_to_purge);
|
||||
}
|
||||
nozzle_cur_filament_ids[nozzle_id] = filament_id;
|
||||
}
|
||||
|
||||
//During the filament change, the extruder will extrude an extra length of grab_length for the corresponding detection, so the purge can reduce this length.
|
||||
float grab_purge_volume = m_config.grab_length.get_at(nozzle_id) * 2.4; //(diameter/2)^2*PI=2.4
|
||||
int grab_extruder_id = filament_maps[filament_id] - 1;
|
||||
float grab_purge_volume = m_config.grab_length.get_at(grab_extruder_id) * 2.4; //(diameter/2)^2*PI=2.4
|
||||
volume_to_purge = std::max(0.f, volume_to_purge - grab_purge_volume);
|
||||
|
||||
// Saving mode reduces the prime volume to 15 mm3; Default is inert.
|
||||
float prime_volume = (m_config.prime_volume_mode == PrimeVolumeMode::pvmSaving) ? 15.f : (float) m_config.prime_volume;
|
||||
// Select prime volume per-filament: nozzle change (carousel rotation) uses
|
||||
// filament_prime_volume_nc, filament change (same nozzle slot) uses filament_prime_volume.
|
||||
float wipe_volume_ec = filament_id < m_config.filament_prime_volume.values.size()
|
||||
? m_config.filament_prime_volume.values[filament_id]
|
||||
: (float) m_config.prime_volume;
|
||||
float wipe_volume_nc = filament_id < m_config.filament_prime_volume_nc.values.size()
|
||||
? m_config.filament_prime_volume_nc.values[filament_id]
|
||||
: (float) m_config.prime_volume;
|
||||
|
||||
float prime_volume = wipe_volume_ec;
|
||||
if (group_result) {
|
||||
bool is_nozzle_change = group_result->are_filaments_same_extruder(current_filament_id, filament_id, layer_idx) &&
|
||||
!group_result->are_filaments_same_nozzle(current_filament_id, filament_id, layer_idx);
|
||||
if (is_nozzle_change) {
|
||||
prime_volume = wipe_volume_nc;
|
||||
}
|
||||
}
|
||||
if (m_config.prime_volume_mode == PrimeVolumeMode::pvmSaving) {
|
||||
prime_volume = 15.f;
|
||||
}
|
||||
|
||||
wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, filament_id,
|
||||
prime_volume, volume_to_purge);
|
||||
current_filament_id = filament_id;
|
||||
nozzle_cur_filament_ids[nozzle_id] = filament_id;
|
||||
}
|
||||
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
|
||||
|
||||
|
||||
@@ -7384,7 +7384,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->tooltip = L("The flush multiplier used in fast purge mode.");
|
||||
def->set_default_value(new ConfigOptionFloats{1.2});
|
||||
|
||||
// BBS
|
||||
// Orca: used by the generic (Type2) wipe tower; also the fallback for filament_prime_volume on Type1.
|
||||
def = this->add("prime_volume", coFloat);
|
||||
def->label = L("Prime volume");
|
||||
def->tooltip = L("This is the volume of material to prime the extruder with on the tower.");
|
||||
@@ -8030,6 +8030,16 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comDevelop;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
// Used by the Type1 wipe tower: filament_prime_volume on a filament change,
|
||||
// filament_prime_volume_nc on a hotend/nozzle change. Type2 uses prime_volume instead.
|
||||
def = this->add("filament_prime_volume", coFloats);
|
||||
def->label = L("Filament change");
|
||||
def->tooltip = L("The volume of material required to prime the extruder on the tower, excluding a hotend change.");
|
||||
def->sidetext = L("mm³");
|
||||
def->min = 1.0;
|
||||
def->mode = comSimple;
|
||||
def->set_default_value(new ConfigOptionFloats{45.});
|
||||
|
||||
def = this->add("filament_prime_volume_nc", coFloats);
|
||||
def->label = L("Hotend change");
|
||||
def->tooltip = L("The volume of material required to prime the extruder for a hotend change on the tower.");
|
||||
@@ -9050,7 +9060,7 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
|
||||
"retraction_distance_when_cut",
|
||||
"internal_bridge_support_thickness", "top_area_threshold", "reduce_wall_solid_infill","filament_load_time","filament_unload_time",
|
||||
"smooth_coefficient", "overhang_totally_speed", "silent_mode",
|
||||
"overhang_speed_classic", "filament_prime_volume",
|
||||
"overhang_speed_classic",
|
||||
"anisotropic_surfaces", // superseded by top_surface_fill_order / bottom_surface_fill_order
|
||||
};
|
||||
|
||||
|
||||
@@ -1841,6 +1841,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
|
||||
// BBS: wipe tower is only used for priming
|
||||
((ConfigOptionFloat, prime_volume))
|
||||
// Nozzle-change (nc) prime volume + pre-heat delta
|
||||
((ConfigOptionFloats, filament_prime_volume))
|
||||
((ConfigOptionFloats, filament_prime_volume_nc))
|
||||
((ConfigOptionFloatsNullable, filament_preheat_temperature_delta))
|
||||
((ConfigOptionFloats, flush_multiplier))
|
||||
|
||||
@@ -771,6 +771,7 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna
|
||||
}
|
||||
}
|
||||
|
||||
gcode << m_writer.reset_e();
|
||||
gcode << m_writer.set_pressure_advance(m_params.start);
|
||||
gcode << "; end pressure advance pattern for layer\n";
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ struct Calib_Params
|
||||
{
|
||||
Calib_Params() : mode(CalibMode::Calib_None){};
|
||||
int extruder_id = 0;
|
||||
double start, end, step;
|
||||
bool print_numbers;
|
||||
double freqStartX, freqEndX, freqStartY, freqEndY;
|
||||
int test_model;
|
||||
double start = 0.0, end = 1.0, step = 0.1;
|
||||
bool print_numbers = false;
|
||||
double freqStartX = 0.0, freqEndX = 1.0, freqStartY = 0.0, freqEndY = 1.0;
|
||||
int test_model = 0;
|
||||
std::string shaper_type;
|
||||
std::vector<double> accelerations;
|
||||
std::vector<double> speeds;
|
||||
|
||||
@@ -558,6 +558,7 @@ namespace Slic3r
|
||||
}
|
||||
else
|
||||
{
|
||||
Slic3r::GUI::wxGetApp().reset_unsigned_plugin_warning();
|
||||
if (m_agent)
|
||||
{
|
||||
if (it->second->connection_type() != "lan" || it->second->connection_type().empty())
|
||||
|
||||
+16
-10
@@ -1043,14 +1043,14 @@ void TextCtrl::propagate_value()
|
||||
|
||||
void TextCtrl::set_value(const boost::any& value, bool change_event/* = false*/) {
|
||||
m_disable_change_event = !change_event;
|
||||
|
||||
if (m_opt.nullable) {
|
||||
const bool m_is_na_val = value.empty() || (boost::any_cast<wxString>(value) == _(L("N/A")));
|
||||
if (!m_is_na_val)
|
||||
m_last_meaningful_value = value;
|
||||
text_ctrl()->SetValue(boost::any_cast<wxString>(value)); // BBS
|
||||
}
|
||||
else
|
||||
text_ctrl()->SetValue(value.empty() ? "" : boost::any_cast<wxString>(value)); // BBS // BBS: null value
|
||||
|
||||
text_ctrl()->SetValue(value.empty() ? wxString() : boost::any_cast<wxString>(value));
|
||||
m_disable_change_event = false;
|
||||
|
||||
if (!change_event) {
|
||||
@@ -1187,18 +1187,24 @@ void CheckBox::set_value(const boost::any& value, bool change_event)
|
||||
m_disable_change_event = !change_event;
|
||||
if (m_opt.nullable) {
|
||||
const bool is_value_unsigned_char = value.type() == typeid(unsigned char);
|
||||
bool bool_value = false;
|
||||
|
||||
m_is_na_val = value.empty() || (is_value_unsigned_char &&
|
||||
boost::any_cast<unsigned char>(value) == ConfigOptionBoolsNullable::nil_value());
|
||||
if (!m_is_na_val)
|
||||
m_last_meaningful_value = is_value_unsigned_char ? value : static_cast<unsigned char>(boost::any_cast<bool>(value));
|
||||
|
||||
const auto bool_value = is_value_unsigned_char ?
|
||||
boost::any_cast<unsigned char>(value) != 0 :
|
||||
boost::any_cast<bool>(value);
|
||||
dynamic_cast<::CheckBox*>(window)->SetValue(m_is_na_val ? false : bool_value); // BBS
|
||||
if (!m_is_na_val) {
|
||||
bool_value = is_value_unsigned_char ?
|
||||
boost::any_cast<unsigned char>(value) != 0 :
|
||||
boost::any_cast<bool>(value);
|
||||
m_last_meaningful_value = is_value_unsigned_char ? value : static_cast<unsigned char>(bool_value);
|
||||
}
|
||||
|
||||
dynamic_cast<::CheckBox*>(window)->SetValue(bool_value);
|
||||
}
|
||||
else if (!value.empty()) // BBS: null value
|
||||
else if (!value.empty()){ // BBS: null value
|
||||
dynamic_cast<::CheckBox*>(window)->SetValue(boost::any_cast<bool>(value)); // BBS
|
||||
}
|
||||
|
||||
dynamic_cast<::CheckBox*>(window)->SetHalfChecked(value.empty());
|
||||
m_disable_change_event = false;
|
||||
}
|
||||
|
||||
+17
-12
@@ -6127,18 +6127,23 @@ bool GUI_App::process_network_msg(std::string dev_id, std::string msg)
|
||||
}
|
||||
else if (msg == "unsigned_studio") {
|
||||
BOOST_LOG_TRIVIAL(info) << "process_network_msg, unsigned_studio";
|
||||
MessageDialog
|
||||
msg_dlg(nullptr,
|
||||
_L("To use OrcaSlicer with Bambu Lab printers, you need to enable LAN mode and Developer mode on your printer.\n\n"
|
||||
"Please go to your printer's settings and:\n"
|
||||
"1. Turn on LAN mode\n"
|
||||
"2. Enable Developer mode\n\n"
|
||||
"Developer mode allows the printer to work exclusively through local network access, "
|
||||
"enabling full functionality with OrcaSlicer."),
|
||||
_L("Network Plug-in Restriction"), wxAPPLY | wxOK);
|
||||
m_show_error_msgdlg = true;
|
||||
msg_dlg.ShowModal();
|
||||
m_show_error_msgdlg = false;
|
||||
// Plugin re-emits this on every subscribe retry; latch it so it shows
|
||||
// once per connection episode.
|
||||
if (!m_show_error_msgdlg && !m_unsigned_plugin_warning_shown) {
|
||||
m_unsigned_plugin_warning_shown = true;
|
||||
MessageDialog
|
||||
msg_dlg(nullptr,
|
||||
_L("To use OrcaSlicer with Bambu Lab printers, you need to enable LAN mode and Developer mode on your printer.\n\n"
|
||||
"Please go to your printer's settings and:\n"
|
||||
"1. Turn on LAN mode\n"
|
||||
"2. Enable Developer mode\n\n"
|
||||
"Developer mode allows the printer to work exclusively through local network access, "
|
||||
"enabling full functionality with OrcaSlicer."),
|
||||
_L("Network Plug-in Restriction"), wxAPPLY | wxOK);
|
||||
m_show_error_msgdlg = true;
|
||||
msg_dlg.ShowModal();
|
||||
m_show_error_msgdlg = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,6 +343,7 @@ private:
|
||||
public:
|
||||
//try again when subscription fails
|
||||
void on_start_subscribe_again(std::string dev_id);
|
||||
void reset_unsigned_plugin_warning() { m_unsigned_plugin_warning_shown = false; }
|
||||
std::string get_local_models_path();
|
||||
bool OnInit() override;
|
||||
int OnExit() override;
|
||||
|
||||
@@ -215,8 +215,8 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
|
||||
const EOptionType option_type = move_type_to_option(curr_type);
|
||||
if (option_type == EOptionType::COUNT || option_type == EOptionType::Travels || option_type == EOptionType::Wipes) {
|
||||
if (ret.vertices.empty() || prev.type != curr.type || prev.extrusion_role != curr.extrusion_role
|
||||
// ORCA: Fix issue with flow rate changes being visualized incorrectly
|
||||
|| prev.mm3_per_mm != curr.mm3_per_mm) {
|
||||
// ORCA: Split the path when a preview value changes.
|
||||
|| prev.mm3_per_mm != curr.mm3_per_mm || prev.acceleration != curr.acceleration || prev.jerk != curr.jerk) {
|
||||
// to allow libvgcode to properly detect the start/end of a path we need to add a 'phantom' vertex
|
||||
// equal to the current one with the exception of the position, which should match the previous move position,
|
||||
// and the times, which are set to zero
|
||||
|
||||
@@ -2338,7 +2338,7 @@ void NotificationManager::SharedProfilesNotification::render_text(ImGuiWrapper&
|
||||
{
|
||||
float hyper_y = starting_y + m_endlines.size() * shift_y + m_line_height * .5f;
|
||||
float dont_show_y = hyper_y + ImGui::CalcTextSize((m_hypertext + " ").c_str()).y + m_line_height * .5f;
|
||||
std::string dont_show_text = _u8L("Don't show again") + std::to_string(m_endlines.size());
|
||||
std::string dont_show_text = _u8L("Don't show again");
|
||||
ImVec2 part_size = ImGui::CalcTextSize(dont_show_text.c_str());
|
||||
|
||||
if (!m_multiline && m_lines_count > 2) {
|
||||
|
||||
@@ -2662,7 +2662,8 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi
|
||||
|
||||
if (instance_box.min.z() < SINKING_Z_THRESHOLD) {
|
||||
// Orca: For sinking object, we use a more expensive algorithm so part below build plate won't be considered
|
||||
if (plate_box.intersects(instance_box)) {
|
||||
// m_plater is null in CLI mode.
|
||||
if (m_plater && plate_box.intersects(instance_box)) {
|
||||
// TODO: FIXME: this does not take exclusion area into account
|
||||
const BuildVolume build_volume(get_shape(), m_plater->build_volume().printable_height(), m_extruder_areas, m_extruder_heights);
|
||||
const auto state = instance->calc_print_volume_state(build_volume);
|
||||
|
||||
@@ -11988,12 +11988,53 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all)
|
||||
auto nozzle_volumes_values = preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")->values;
|
||||
assert(obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2);
|
||||
if (obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2) {
|
||||
// Map device flow->volume via the table, not `flowtype - 1` (which mis-maps U_FLOW to nvtHybrid).
|
||||
NozzleVolumeType right_nozzle_type = DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0));
|
||||
NozzleVolumeType left_nozzle_type = DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(1));
|
||||
NozzleVolumeType preset_left_type = NozzleVolumeType(nozzle_volumes_values[0]);
|
||||
NozzleVolumeType preset_right_type = NozzleVolumeType(nozzle_volumes_values[1]);
|
||||
is_same_as_printer = (left_nozzle_type == preset_left_type && right_nozzle_type == preset_right_type);
|
||||
// [Vortek] H2C: Use BBS-style NozzleGroupInfo comparison instead of direct nozzle type match.
|
||||
// This correctly handles Hybrid presets (which expand into per-type counts) and detects
|
||||
// never-synced state (nozzle_count==0) so the first sync dialog appears.
|
||||
// After device sync, extruder_nozzle_stats matches printer → dialog suppressed.
|
||||
// Reference to BBS: BambuStudio/src/slic3r/GUI/Plater.cpp is_extruder_stat_synced()
|
||||
using namespace MultiNozzleUtils;
|
||||
auto nozzle_diameter_values = preset_bundle->printers.get_edited_preset().config.option<ConfigOptionFloatsNullable>("nozzle_diameter")->values;
|
||||
|
||||
// Build preset nozzle groups from extruder_nozzle_stats config
|
||||
std::vector<std::vector<NozzleGroupInfo>> preset_nozzle_infos(nozzle_diameter_values.size());
|
||||
for (size_t extruder_id = 0; extruder_id < nozzle_diameter_values.size(); ++extruder_id) {
|
||||
NozzleVolumeType preset_volume_type = NozzleVolumeType(nozzle_volumes_values[extruder_id]);
|
||||
std::string preset_diameter = format_diameter_to_str(nozzle_diameter_values[extruder_id]);
|
||||
|
||||
if (preset_volume_type == nvtHybrid) {
|
||||
// Hybrid: expand into separate groups for each nozzle type from stats
|
||||
int std_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtStandard);
|
||||
int hf_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtHighFlow);
|
||||
if (std_count > 0)
|
||||
preset_nozzle_infos[extruder_id].emplace_back(preset_diameter, nvtStandard, extruder_id, std_count);
|
||||
if (hf_count > 0)
|
||||
preset_nozzle_infos[extruder_id].emplace_back(preset_diameter, nvtHighFlow, extruder_id, hf_count);
|
||||
// If both are 0 → never synced → empty group → will mismatch
|
||||
} else {
|
||||
int count = getExtruderNozzleCount(preset_bundle, extruder_id, preset_volume_type);
|
||||
preset_nozzle_infos[extruder_id].emplace_back(preset_diameter, preset_volume_type, extruder_id, count);
|
||||
}
|
||||
}
|
||||
|
||||
// Compare with printer nozzle groups
|
||||
auto printer_groups = obj->GetNozzleSystem()->GetNozzleGroups();
|
||||
for (const auto& preset_groups : preset_nozzle_infos) {
|
||||
for (const auto& preset_group : preset_groups) {
|
||||
if (preset_group.nozzle_count == 0) {
|
||||
// Never synced: if printer has nozzles of this type → needs sync
|
||||
if (std::find_if(printer_groups.begin(), printer_groups.end(),
|
||||
[&preset_group](const NozzleGroupInfo& elem) { return preset_group.is_same_type(elem); })
|
||||
!= printer_groups.end()) {
|
||||
is_same_as_printer = false;
|
||||
break;
|
||||
}
|
||||
} else if (std::find(printer_groups.begin(), printer_groups.end(), preset_group) == printer_groups.end()) {
|
||||
is_same_as_printer = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::map<int, int>> ams_count_info;
|
||||
|
||||
@@ -48,13 +48,14 @@ std::string plugin_defaults_user_script()
|
||||
return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend");
|
||||
}
|
||||
|
||||
// Injected into every page at document start (before the plugin's own scripts).
|
||||
// Defines window.orca as the only host surface the page may use. It references
|
||||
// window.wx lazily (at call time) so it never races the backend's deferred
|
||||
// registration of the "wx" message handler. Guarded against double-injection so
|
||||
// it is harmless if also prepended.
|
||||
// Injected into the top-level page at document start (before the plugin's own
|
||||
// scripts). Defines window.orca as the only host surface the page may use. It
|
||||
// references window.wx lazily (at call time) so it never races the backend's
|
||||
// deferred registration of the "wx" message handler. Guarded against
|
||||
// double-injection so it is harmless if also prepended.
|
||||
constexpr char ORCA_BRIDGE_JS[] = R"JS(
|
||||
(function () {
|
||||
if (window.top !== window.self) return;
|
||||
if (window.orca) return;
|
||||
var handlers = [];
|
||||
function send(kind, data) {
|
||||
|
||||
@@ -300,7 +300,12 @@ PluginAvailableActions evaluate_action_policy(const PluginDialogItem& item)
|
||||
|
||||
add_action("open_folder", "Show in folder", has_local);
|
||||
|
||||
add_action("reinstall_plugin", "Reinstall");
|
||||
if (is_cloud) {
|
||||
add_action("reinstall_plugin", "Reinstall");
|
||||
} else {
|
||||
add_action("reload_plugin", "Reload");
|
||||
add_action("clear_cache_reload_plugin", "Delete cache and reload");
|
||||
}
|
||||
|
||||
return available_actions;
|
||||
}
|
||||
@@ -739,11 +744,13 @@ void PluginsDialog::handle_plugin_menu_action(const std::string& plugin_key, con
|
||||
unsubscribe_cloud_plugin(row_data);
|
||||
} else if (action == "delete_mine_plugin") {
|
||||
delete_mine_local_and_cloud_plugin(plugin_key);
|
||||
} else if (action == "reload_plugin") {
|
||||
reload_local_plugin(plugin_key, /*clear_cache=*/false);
|
||||
} else if (action == "clear_cache_reload_plugin") {
|
||||
reload_local_plugin(plugin_key, /*clear_cache=*/true);
|
||||
} else if (action == "reinstall_plugin") {
|
||||
if (row_data.is_cloud_plugin())
|
||||
reinstall_cloud_plugin(row_data);
|
||||
else
|
||||
reinstall_local_plugin(plugin_key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1123,7 +1130,7 @@ void PluginsDialog::unsubscribe_cloud_plugin(const PluginDescriptor& plugin)
|
||||
_L("Unsubscribing plugin"), _L("Deleting local files and unsubscribing plugin..."));
|
||||
}
|
||||
|
||||
void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key)
|
||||
void PluginsDialog::reload_local_plugin(const std::string& plugin_key, bool clear_cache)
|
||||
{
|
||||
if (plugin_key.empty())
|
||||
return;
|
||||
@@ -1132,11 +1139,34 @@ void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key)
|
||||
std::pair<bool, std::string> reload_result{false, ""};
|
||||
try {
|
||||
reload_result = run_with_dialog_wait(
|
||||
[plugin_key, was_loaded]() -> std::pair<bool, std::string> {
|
||||
[plugin_key, was_loaded, clear_cache]() -> std::pair<bool, std::string> {
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
|
||||
boost::filesystem::path cache_dir;
|
||||
if (clear_cache) {
|
||||
PluginDescriptor descriptor;
|
||||
if (!manager.try_get_plugin_descriptor(plugin_key, descriptor))
|
||||
return {false, "Plugin not found."};
|
||||
|
||||
boost::filesystem::path resolved_root;
|
||||
std::string resolve_error;
|
||||
if (!resolve_allowed_plugin_root(descriptor, {get_orca_plugins_dir()},
|
||||
"Refusing to clear a plugin cache outside the local plugin directory.",
|
||||
resolved_root, resolve_error))
|
||||
return {false, resolve_error};
|
||||
cache_dir = resolved_root / "__whl_extracted__";
|
||||
}
|
||||
|
||||
if (!manager.unload_plugin(plugin_key))
|
||||
return {false, "Failed to unload plugin."};
|
||||
|
||||
if (clear_cache) {
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove_all(cache_dir, ec);
|
||||
if (ec)
|
||||
return {false, "Failed to clear plugin cache: " + ec.message()};
|
||||
}
|
||||
|
||||
manager.load_plugin(plugin_key, false);
|
||||
std::string error;
|
||||
if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) || !manager.is_plugin_loaded(plugin_key))
|
||||
|
||||
@@ -96,7 +96,7 @@ private:
|
||||
void open_plugin_folder(const Slic3r::PluginDescriptor& plugin);
|
||||
void delete_local_plugin(const Slic3r::PluginDescriptor& plugin);
|
||||
void unsubscribe_cloud_plugin(const Slic3r::PluginDescriptor& plugin);
|
||||
void reinstall_local_plugin(const std::string& plugin_key);
|
||||
void reload_local_plugin(const std::string& plugin_key, bool clear_cache);
|
||||
void reinstall_cloud_plugin(const Slic3r::PluginDescriptor& plugin);
|
||||
void delete_mine_local_and_cloud_plugin(const std::string& plugin_key);
|
||||
|
||||
|
||||
@@ -218,6 +218,49 @@ void PrePrintChecker::add_with_link(PrintDialogStatus state, wxString msg, wxStr
|
||||
}
|
||||
}
|
||||
|
||||
// Orca: acknowledgement-checkbox variant of add(); see add_with_link() for the shared classification.
|
||||
void PrePrintChecker::add_with_checkbox(PrintDialogStatus state, wxString msg, wxString checkbox_label, bool checked, std::function<void(bool)> checkbox_callback)
|
||||
{
|
||||
prePrintInfo info;
|
||||
|
||||
if (is_error(state)) {
|
||||
info.level = prePrintInfoLevel::Error;
|
||||
} else if (is_warning(state)) {
|
||||
info.level = prePrintInfoLevel::Warning;
|
||||
} else {
|
||||
info.level = prePrintInfoLevel::Normal;
|
||||
}
|
||||
|
||||
if (is_error_printer(state)) {
|
||||
info.type = prePrintInfoType::Printer;
|
||||
} else if (is_error_filament(state)) {
|
||||
info.type = prePrintInfoType::Filament;
|
||||
} else if (is_warning_printer(state)) {
|
||||
info.type = prePrintInfoType::Printer;
|
||||
} else if (is_warning_filament(state)) {
|
||||
info.type = prePrintInfoType::Filament;
|
||||
}
|
||||
|
||||
info.msg = msg;
|
||||
info.checkbox_label = checkbox_label;
|
||||
info.checkbox_checked = checked;
|
||||
info.checkbox_callback = checkbox_callback;
|
||||
|
||||
switch (info.type) {
|
||||
case prePrintInfoType::Filament:
|
||||
if (std::find(filamentList.begin(), filamentList.end(), info) == filamentList.end()) {
|
||||
filamentList.push_back(info);
|
||||
}
|
||||
break;
|
||||
case prePrintInfoType::Printer:
|
||||
if (std::find(printerList.begin(), printerList.end(), info) == printerList.end()) {
|
||||
printerList.push_back(info);
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//void PrePrintMsgBoard::add(const wxString &msg, const wxString &tips, bool is_error)
|
||||
//{
|
||||
@@ -310,6 +353,18 @@ bool PrinterMsgPanel::UpdateInfos(const std::vector<prePrintInfo>& infos)
|
||||
label->Show();
|
||||
m_sizer->Add(label, 0, wxBOTTOM, FromDIP(4));
|
||||
}
|
||||
|
||||
// Orca: acknowledgement checkbox rendered beneath the message (e.g. a nozzle diameter that
|
||||
// differs from the one the printer remembers, which the user may legitimately override).
|
||||
if (!info.checkbox_label.empty())
|
||||
{
|
||||
wxCheckBox* checkbox = new wxCheckBox(this, wxID_ANY, info.checkbox_label);
|
||||
checkbox->SetForegroundColour(_GetLabelColour(info));
|
||||
checkbox->SetValue(info.checkbox_checked);
|
||||
auto cb = info.checkbox_callback;
|
||||
checkbox->Bind(wxEVT_CHECKBOX, [cb](wxCommandEvent& e) { if (cb) cb(e.IsChecked()); });
|
||||
m_sizer->Add(checkbox, 0, wxBOTTOM, FromDIP(4));
|
||||
}
|
||||
}
|
||||
|
||||
this->Show();
|
||||
|
||||
@@ -26,6 +26,9 @@ struct prePrintInfo
|
||||
wxString wiki_url;
|
||||
wxString link_label; // optional: clickable text appended after msg
|
||||
std::function<void()> link_callback; // optional: internal action for link_label click
|
||||
wxString checkbox_label; // optional: an acknowledgement checkbox under msg
|
||||
bool checkbox_checked{false};
|
||||
std::function<void(bool)> checkbox_callback; // called with the new state on toggle
|
||||
int index{0};
|
||||
|
||||
public:
|
||||
@@ -33,8 +36,9 @@ public:
|
||||
return level == other.level && type == other.type &&
|
||||
msg == other.msg && tips == other.tips &&
|
||||
wiki_url == other.wiki_url && link_label == other.link_label &&
|
||||
checkbox_label == other.checkbox_label && checkbox_checked == other.checkbox_checked &&
|
||||
index == other.index;
|
||||
// link_callback excluded: std::function is not comparable
|
||||
// link_callback / checkbox_callback excluded: std::function is not comparable
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,7 +63,6 @@ enum PrintDialogStatus : unsigned int {
|
||||
PrintStatusInPrinting,
|
||||
PrintStatusNozzleMatchInvalid,
|
||||
PrintStatusNozzleDataInvalid,
|
||||
PrintStatusNozzleDiameterMismatch,
|
||||
PrintStatusNozzleTypeMismatch,
|
||||
PrintStatusNozzleNoMatchedHotends,
|
||||
PrintStatusNozzleRackMaximumInstalled,
|
||||
@@ -106,6 +109,9 @@ enum PrintDialogStatus : unsigned int {
|
||||
PrintStatusFilaSwitcherSlicingNotMatch,
|
||||
PrintStatusRackNozzleNumUnmeetWarning,
|
||||
PrintStatusHasUnreliableNozzleWarning,
|
||||
// Orca: a nozzle diameter that differs from the one the printer remembers is a warning,
|
||||
// not an error, so non-standard nozzles can still be printed with.
|
||||
PrintStatusNozzleDiameterMismatch,
|
||||
PrintStatusPrinterWarningEnd,
|
||||
|
||||
// Warnings for filament
|
||||
@@ -159,6 +165,9 @@ public:
|
||||
void add(PrintDialogStatus state, wxString msg, wxString tip, const wxString& wiki_url);
|
||||
// Orca: minimal callback-link render path instead of the full style-bitmask machinery.
|
||||
void add_with_link(PrintDialogStatus state, wxString msg, wxString link_label, std::function<void()> link_callback);
|
||||
// Orca: render msg with an acknowledgement checkbox beneath it, for an overridable warning that
|
||||
// the user must tick before proceeding (checkbox_callback reports the new state).
|
||||
void add_with_checkbox(PrintDialogStatus state, wxString msg, wxString checkbox_label, bool checked, std::function<void(bool)> checkbox_callback);
|
||||
static ::std::string get_print_status_info(PrintDialogStatus status);
|
||||
|
||||
wxString get_pre_state_msg(PrintDialogStatus status);
|
||||
|
||||
@@ -246,7 +246,10 @@ void PrinterWebView::SendAPIKey()
|
||||
// RemoveAllUserScripts causes WebView to forget about our script message handler,
|
||||
// so re-add it here.
|
||||
m_browser->RemoveScriptMessageHandler("wx");
|
||||
m_browser->AddScriptMessageHandler("wx");
|
||||
if (m_browser->AddScriptMessageHandler("wx"))
|
||||
WebView::MarkScriptMessageHandlerAdded(m_browser);
|
||||
else
|
||||
wxLogError("Could not add script message handler");
|
||||
|
||||
#ifdef __linux__
|
||||
// Re-inject the vue-resize/WebKitGTK workaround that RemoveAllUserScripts just cleared.
|
||||
|
||||
@@ -2269,8 +2269,11 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector<wxSt
|
||||
Enable_Refresh_Button(true);
|
||||
Enable_Send_Button(false);
|
||||
} else if (status == PrintStatusNozzleDiameterMismatch) {
|
||||
// Orca: overridable — a non-standard nozzle is a valid reason to differ. Send is gated on
|
||||
// the acknowledgement checkbox added to the message board below (add_with_checkbox), which
|
||||
// is enabled only while the user's acknowledgement still matches the current mismatch.
|
||||
Enable_Refresh_Button(true);
|
||||
Enable_Send_Button(false);
|
||||
Enable_Send_Button(!m_nozzle_diameter_ack_msg.empty() && m_nozzle_diameter_ack_msg == m_nozzle_diameter_mismatch_msg);
|
||||
} else if (status == PrintStatusNozzleTypeMismatch) {
|
||||
Enable_Refresh_Button(true);
|
||||
Enable_Send_Button(false);
|
||||
@@ -2470,7 +2473,19 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector<wxSt
|
||||
|
||||
/*enter perpare mode*/
|
||||
prepare_mode(false);
|
||||
m_pre_print_checker.add(status, msg, tips, wiki_url);
|
||||
if (status == PrintDialogStatus::PrintStatusNozzleDiameterMismatch) {
|
||||
// Short label on purpose: the 420px message panel caps its width and a wxCheckBox label
|
||||
// does not wrap; the full explanation is in the warning text above it.
|
||||
m_pre_print_checker.add_with_checkbox(status, msg,
|
||||
_L("I have checked the installed nozzle and want to print anyway."),
|
||||
!m_nozzle_diameter_ack_msg.empty() && m_nozzle_diameter_ack_msg == m_nozzle_diameter_mismatch_msg,
|
||||
[this](bool checked) {
|
||||
m_nozzle_diameter_ack_msg = checked ? m_nozzle_diameter_mismatch_msg : wxString();
|
||||
Enable_Send_Button(checked);
|
||||
});
|
||||
} else {
|
||||
m_pre_print_checker.add(status, msg, tips, wiki_url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4565,8 +4580,14 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj
|
||||
}
|
||||
|
||||
msg_params.emplace_back(_L("Tips: If you changed your nozzle of your printer lately, please go to 'Device -> Printer parts' to change your nozzle setting."));
|
||||
|
||||
// Orca: non-blocking. A diameter that differs from the one the printer
|
||||
// remembers is legitimate with a non-standard nozzle, so the print is held back
|
||||
// only by the acknowledgement checkbox shown in the message board, not by a
|
||||
// disabled Send outright. Keep checking the remaining extruders.
|
||||
m_nozzle_diameter_mismatch_msg = msg_params.front();
|
||||
show_status(PrintDialogStatus::PrintStatusNozzleDiameterMismatch, msg_params);
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4608,6 +4629,9 @@ static wxString _get_ext_loc_str(const std::unordered_set<int>& extruders, int t
|
||||
void SelectMachineDialog::update_show_status(MachineObject* obj_)
|
||||
{
|
||||
m_pre_print_checker.clear();
|
||||
// Orca: re-raised by CheckErrorExtruderNozzleWithSlicing() below if the mismatch is still there,
|
||||
// so an early return from this pass cannot leave a stale warning behind.
|
||||
m_nozzle_diameter_mismatch_msg.clear();
|
||||
|
||||
/*agent check and printer valid check*/
|
||||
NetworkAgent* agent = Slic3r::GUI::wxGetApp().getAgent();
|
||||
|
||||
@@ -550,6 +550,13 @@ public:
|
||||
// Compare the slicing file's nozzle requirements (validity, flow, diameter) against the
|
||||
// printer; the rack extruder is checked against its whole inventory (mounted + rack).
|
||||
bool CheckErrorExtruderNozzleWithSlicing(MachineObject* obj_);//return true if no errors
|
||||
// Orca: a nozzle diameter differing from the one the printer remembers is a non-blocking
|
||||
// warning shown in the message board with an acknowledgement checkbox; Send stays disabled
|
||||
// until the user ticks it. m_nozzle_diameter_mismatch_msg is the current mismatch text (empty
|
||||
// when there is none), m_nozzle_diameter_ack_msg the text the user acknowledged; Send is
|
||||
// allowed only while the two match, so a changed or cleared mismatch re-requires acknowledgement.
|
||||
wxString m_nozzle_diameter_mismatch_msg;
|
||||
wxString m_nozzle_diameter_ack_msg;
|
||||
// Warn (without blocking) when a mapped filament would be printed from both extruders on a
|
||||
// dual-extruder printer, so per-nozzle manual K-value can't follow it across the whole print.
|
||||
bool CheckWarningFilamentCrossExtruder(MachineObject* obj_);
|
||||
|
||||
+25
-2
@@ -34,6 +34,7 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI_ObjectList.hpp"
|
||||
#include "slic3r/Utils/PresetUpdater.hpp"
|
||||
#include "slic3r/plugin/PluginConfig.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "format.hpp"
|
||||
@@ -1795,9 +1796,19 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
|
||||
|
||||
// Keep this preset's "plugins" manifest in sync when a plugin picker changes, so full_config() and
|
||||
// save_to_json() always find resolved "name;uuid;capability" references and rebuild it nowhere else.
|
||||
// Also drop any plugin_config_overrides entries for a capability the change just stopped
|
||||
// referencing (e.g. a plugin removed from slicing_pipeline_plugin), so a saved preset never
|
||||
// carries configuration for a capability it no longer names. The Configure button is a separate
|
||||
// field holding its own cached copy of that value, so it needs to be told explicitly, or it
|
||||
// keeps showing the stale count until something else happens to refresh it.
|
||||
if (const ConfigOptionDef* opt_def = m_config->def()->get(opt_key);
|
||||
opt_def && opt_def->is_plugin_backed())
|
||||
opt_def && opt_def->is_plugin_backed()) {
|
||||
m_config->update_plugin_manifest();
|
||||
if (prune_stale_plugin_overrides(*m_config)) {
|
||||
if (Field* overrides_field = get_field(PLUGIN_OVERRIDES_OPTION_KEY))
|
||||
overrides_field->set_value(boost::any(m_config->opt_string(PLUGIN_OVERRIDES_OPTION_KEY)), false);
|
||||
}
|
||||
}
|
||||
|
||||
if (opt_key == "gcode_flavor" && m_type == Preset::TYPE_PRINTER) {
|
||||
if (auto printer_tab = dynamic_cast<TabPrinter*>(this))
|
||||
@@ -3288,7 +3299,19 @@ static std::vector<std::string> intersect(std::vector<std::string> const& l, std
|
||||
static std::vector<std::string> concat(std::vector<std::string> const& l, std::vector<std::string> const& r)
|
||||
{
|
||||
std::vector<std::string> t;
|
||||
std::set_union(l.begin(), l.end(), r.begin(), r.end(), std::back_inserter(t));
|
||||
bool l_is_sorted = std::is_sorted(l.begin(), l.end());
|
||||
bool r_is_sorted = std::is_sorted(r.begin(), r.end());
|
||||
|
||||
if (l_is_sorted && r_is_sorted) {
|
||||
std::set_union(l.begin(), l.end(), r.begin(), r.end(), std::back_inserter(t));
|
||||
return t;
|
||||
}
|
||||
|
||||
std::vector<std::string> l_sorted = l;
|
||||
std::vector<std::string> r_sorted = r;
|
||||
std::sort(l_sorted.begin(), l_sorted.end());
|
||||
std::sort(r_sorted.begin(), r_sorted.end());
|
||||
std::set_union(l_sorted.begin(), l_sorted.end(), r_sorted.begin(), r_sorted.end(), std::back_inserter(t));
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ TroubleshootDialog::TroubleshootDialog()
|
||||
Fit();
|
||||
});
|
||||
|
||||
auto link_wiki = new HyperLink(this, _L("Wiki Guide"));
|
||||
auto link_wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/troubleshoot_center");
|
||||
|
||||
// RIGHT SIZER //////////////////////
|
||||
|
||||
|
||||
@@ -242,8 +242,15 @@ public:
|
||||
g_webviews.erase(iter);
|
||||
}
|
||||
wxWebView *m_webView;
|
||||
// Guards against registering the "wx" handler twice (a duplicate throws on WKWebView).
|
||||
bool m_script_handler_added = false;
|
||||
};
|
||||
|
||||
static WebViewRef *webview_ref(wxWebView *webView)
|
||||
{
|
||||
return webView ? static_cast<WebViewRef *>(webView->GetRefData()) : nullptr;
|
||||
}
|
||||
|
||||
wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
|
||||
{
|
||||
#if wxUSE_WEBVIEW_EDGE
|
||||
@@ -304,10 +311,17 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
|
||||
Slic3r::GUI::WKWebView_setTransparentBackground(wkWebView);
|
||||
#endif
|
||||
auto addScriptMessageHandler = [] (wxWebView *webView) {
|
||||
// Skip if SendAPIKey() already registered "wx"; a duplicate add throws an
|
||||
// uncatchable NSException on WKWebView, killing the app at startup.
|
||||
WebViewRef *ref = webview_ref(webView);
|
||||
if (ref && ref->m_script_handler_added)
|
||||
return;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": begin to add script message handler for wx.";
|
||||
Slic3r::GUI::wxGetApp().set_adding_script_handler(true);
|
||||
if (!webView->AddScriptMessageHandler("wx"))
|
||||
wxLogError("Could not add script message handler");
|
||||
else if (ref)
|
||||
ref->m_script_handler_added = true;
|
||||
Slic3r::GUI::wxGetApp().set_adding_script_handler(false);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": finished add script message handler for wx.";
|
||||
};
|
||||
@@ -336,6 +350,12 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
|
||||
g_webviews.push_back(webView);
|
||||
return webView;
|
||||
}
|
||||
|
||||
void WebView::MarkScriptMessageHandlerAdded(wxWebView * webView)
|
||||
{
|
||||
if (WebViewRef *ref = webview_ref(webView))
|
||||
ref->m_script_handler_added = true;
|
||||
}
|
||||
#if wxUSE_WEBVIEW_EDGE
|
||||
bool WebView::CheckWebViewRuntime()
|
||||
{
|
||||
|
||||
@@ -18,6 +18,9 @@ public:
|
||||
|
||||
static bool RunScript(wxWebView * webView, wxString const & msg);
|
||||
|
||||
// Marks "wx" as registered so CreateWebView's deferred add skips the duplicate.
|
||||
static void MarkScriptMessageHandlerAdded(wxWebView * webView);
|
||||
|
||||
static void RecreateAll();
|
||||
};
|
||||
|
||||
|
||||
@@ -96,6 +96,9 @@ std::string WebViewHostDialog::document_start_injector(const std::string& markup
|
||||
const std::string literal = nlohmann::json(markup).dump();
|
||||
std::string s;
|
||||
s += "(function(){";
|
||||
// wxWebView's AddUserScript runs in child frames too (including cross-origin
|
||||
// frames on WebView2). Host theme state belongs only to the top-level page.
|
||||
s += "if(window.top!==window.self)return;";
|
||||
s += prelude;
|
||||
s += "var css=" + literal + ";";
|
||||
s += "function inject(){";
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
|
||||
#include <libslic3r/Config.hpp>
|
||||
#include <libslic3r/PresetBundle.hpp>
|
||||
#include <libslic3r/PrintConfig.hpp>
|
||||
#include <slic3r/GUI/GUI.hpp>
|
||||
@@ -164,6 +165,20 @@ bool CapabilityConfigDocument::erase(const PluginCapabilityId& id)
|
||||
return erased;
|
||||
}
|
||||
|
||||
bool CapabilityConfigDocument::prune_unreferenced(const std::set<std::pair<PluginCapabilityType, std::string>>& referenced)
|
||||
{
|
||||
bool changed = false;
|
||||
for (auto it = m_entries.begin(); it != m_entries.end();) {
|
||||
if (referenced.count({it->first.type, it->first.name}) != 0) {
|
||||
++it;
|
||||
} else {
|
||||
it = m_entries.erase(it);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool CapabilityConfigDocument::empty() const
|
||||
{
|
||||
return m_entries.empty() && m_opaque_entries.empty();
|
||||
@@ -342,6 +357,50 @@ std::string serialize_plugin_overrides(const CapabilityConfigDocument& document)
|
||||
return document.empty() ? std::string() : document.serialize_entries().dump();
|
||||
}
|
||||
|
||||
bool prune_stale_plugin_overrides(DynamicConfig& config)
|
||||
{
|
||||
const auto* overrides_opt = dynamic_cast<const ConfigOptionString*>(config.option(PLUGIN_OVERRIDES_OPTION_KEY));
|
||||
if (overrides_opt == nullptr || overrides_opt->value.empty())
|
||||
return false;
|
||||
|
||||
CapabilityConfigDocument overrides;
|
||||
std::string error;
|
||||
if (!parse_plugin_overrides(overrides_opt->value, overrides, error)) {
|
||||
// Malformed text is not ours to fix up here: leave it untouched rather than risk
|
||||
// discarding data the user might still be able to recover.
|
||||
BOOST_LOG_TRIVIAL(error) << "prune_stale_plugin_overrides: " << error;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Capability names currently referenced by a plugin-backed option's value(s) — e.g.
|
||||
// slicing_pipeline_plugin's ConfigOptionStrings entries name SlicingPipeline capabilities
|
||||
// directly, the same raw values save_plugin_collection() resolves into the "plugins" manifest.
|
||||
std::set<std::pair<PluginCapabilityType, std::string>> referenced;
|
||||
const ConfigDef* def = config.def();
|
||||
for (const std::string& opt_key : config.keys()) {
|
||||
const ConfigOptionDef* opt_def = def != nullptr ? def->get(opt_key) : nullptr;
|
||||
if (opt_def == nullptr || !opt_def->is_plugin_backed())
|
||||
continue;
|
||||
|
||||
const ConfigOption* opt = config.option(opt_key);
|
||||
const PluginCapabilityType type = plugin_capability_type_from_string(opt_def->plugin_type);
|
||||
if (const auto* string_opt = dynamic_cast<const ConfigOptionString*>(opt)) {
|
||||
if (!string_opt->value.empty())
|
||||
referenced.emplace(type, string_opt->value);
|
||||
} else if (const auto* vector_opt = dynamic_cast<const ConfigOptionVectorBase*>(opt)) {
|
||||
for (const std::string& value : vector_opt->vserialize())
|
||||
if (!value.empty())
|
||||
referenced.emplace(type, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!overrides.prune_unreferenced(referenced))
|
||||
return false;
|
||||
|
||||
config.set_key_value(PLUGIN_OVERRIDES_OPTION_KEY, new ConfigOptionString(serialize_plugin_overrides(overrides)));
|
||||
return true;
|
||||
}
|
||||
|
||||
EffectiveCapabilityConfig PresetPluginConfigService::get_effective_config(const CapabilityConfigDocument& overrides,
|
||||
const PluginCapabilityId& id) const
|
||||
{
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#define PLUGIN_CONFIG_DIR "config.json"
|
||||
@@ -16,6 +18,7 @@
|
||||
namespace Slic3r {
|
||||
|
||||
class Preset;
|
||||
class DynamicConfig;
|
||||
struct CapabilityConfigEntry
|
||||
{
|
||||
PluginCapabilityId id;
|
||||
@@ -35,6 +38,9 @@ public:
|
||||
bool contains(const PluginCapabilityId& id) const;
|
||||
bool upsert(CapabilityConfigEntry entry);
|
||||
bool erase(const PluginCapabilityId& id);
|
||||
// Drops every entry whose (type, name) is not in `referenced`, e.g. capabilities a preset's
|
||||
// plugin-backed options no longer name. Returns true if anything was removed.
|
||||
bool prune_unreferenced(const std::set<std::pair<PluginCapabilityType, std::string>>& referenced);
|
||||
bool empty() const;
|
||||
nlohmann::json serialize_entries() const;
|
||||
nlohmann::json root_json() const;
|
||||
@@ -50,6 +56,14 @@ std::string plugin_overrides_of(const Preset& preset);
|
||||
bool parse_plugin_overrides(const std::string& raw, CapabilityConfigDocument& document, std::string& error);
|
||||
std::string serialize_plugin_overrides(const CapabilityConfigDocument& document);
|
||||
|
||||
// Drops plugin_config_overrides entries for capabilities no longer named by any plugin-backed
|
||||
// option's current value in `config` (e.g. slicing_pipeline_plugin cleared or switched to a
|
||||
// different capability), and writes the result back if anything changed. Called wherever a
|
||||
// plugin-backed option's value changes, so a saved preset never carries configuration for a
|
||||
// capability it no longer references. Returns true if `config` was modified, so a caller holding a
|
||||
// GUI field over PLUGIN_OVERRIDES_OPTION_KEY knows it must refresh that field's displayed value.
|
||||
bool prune_stale_plugin_overrides(DynamicConfig& config);
|
||||
|
||||
struct EffectiveCapabilityConfig
|
||||
{
|
||||
PluginCapabilityId id;
|
||||
|
||||
@@ -54,7 +54,7 @@ struct PluginDescriptor
|
||||
std::string description; // Plugin description
|
||||
std::string author; // Plugin author from manifest, if available
|
||||
std::string version; // Selected plugin version
|
||||
std::string latest_version; // Latest available cloud version fallback when changelog is unavailable.
|
||||
std::string latest_version; // Authoritative latest available cloud version.
|
||||
std::string installed_version; // Locally installed package version. Preserved across cloud merges, which overwrite `version` with the latest cloud version. Empty when not installed.
|
||||
std::vector<std::string> display_types; // Display-only "compatibility" labels (cloud: raw service labels; local: from real capabilities). Never used for dispatch.
|
||||
std::string plugin_root; // Installed plugin directory, even when entry_path is invalid or ambiguous.
|
||||
@@ -117,10 +117,6 @@ struct PluginDescriptor
|
||||
bool is_unauthorized() const { return get_update_status() == PluginUpdateStatus::Unauthorized; }
|
||||
std::string latest_available_version() const
|
||||
{
|
||||
for (const PluginChangelog& entry : changelog) {
|
||||
if (!entry.version.empty())
|
||||
return entry.version;
|
||||
}
|
||||
if (!latest_version.empty())
|
||||
return latest_version;
|
||||
return version;
|
||||
|
||||
Reference in New Issue
Block a user