Merge branch 'main' into fix/plugin-bugs

This commit is contained in:
Ian Chua
2026-07-22 13:14:03 +08:00
committed by GitHub
25 changed files with 3556 additions and 83 deletions
+20 -3
View File
@@ -24,6 +24,8 @@
#include <iostream> #include <iostream>
#include <math.h> #include <math.h>
#include <csignal> #include <csignal>
#include <atomic>
#include <new>
#if defined(__linux__) || defined(__LINUX__) #if defined(__linux__) || defined(__LINUX__)
#include <condition_variable> #include <condition_variable>
@@ -7608,6 +7610,9 @@ LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
}*/ }*/
#if defined(_MSC_VER) || defined(__MINGW32__) #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" { extern "C" {
__declspec(dllexport) int __stdcall orcaslicer_main(int argc, wchar_t **argv) __declspec(dllexport) int __stdcall orcaslicer_main(int argc, wchar_t **argv)
{ {
@@ -7626,10 +7631,22 @@ extern "C" {
//AddVectoredExceptionHandler(1, CBaseException::UnhandledExceptionFilter); //AddVectoredExceptionHandler(1, CBaseException::UnhandledExceptionFilter);
SET_DEFULTER_HANDLER(); SET_DEFULTER_HANDLER();
#endif #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([]() { std::set_new_handler([]() {
int *a = nullptr; if (!g_dump_in_progress.exchange(true)) {
*a = 0; 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. // Call the UTF8 main.
return CLI().run(argc, argv_ptrs.data()); return CLI().run(argc, argv_ptrs.data());
} }
+15
View File
@@ -614,6 +614,8 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
float leftover = 0.0f; float leftover = 0.0f;
for (size_t i = additional_buffer_idx; i < additional_buffer.size(); ++i) for (size_t i = additional_buffer_idx; i < additional_buffer.size(); ++i)
leftover += additional_buffer[i].second; 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); time += double(leftover);
gcode_time.cache += leftover; gcode_time.cache += leftover;
} else { } else {
@@ -3492,6 +3494,7 @@ void GCodeProcessor::reset()
m_extruder_blocks.clear(); m_extruder_blocks.clear();
m_machine_start_gcode_end_line_id = (unsigned int) (-1); m_machine_start_gcode_end_line_id = (unsigned int) (-1);
m_machine_end_gcode_start_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_remaining_volume = std::vector<float>(MAXIMUM_EXTRUDER_NUMBER, 0.f);
m_line_id = 0; m_line_id = 0;
@@ -4219,6 +4222,14 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
return; 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 // Orca: Integrate filament consumption for purging performed to an external device and controlled via macros
// (eg. Happy Hare) in the filament consumption stats. // (eg. Happy Hare) in the filament consumption stats.
if (boost::starts_with(comment, GCodeProcessor::External_Purge_Tag)) { 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; float value_p = 0.0;
if (line.has_value('S', value_s) || line.has_value('P', value_p)) { if (line.has_value('S', value_s) || line.has_value('P', value_p)) {
value_s += value_p * 0.001; 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); simulate_st_synchronize(value_s);
} }
} }
+4
View File
@@ -1109,6 +1109,10 @@ class Print;
std::vector<ExtruderPreHeating::ExtruderUsageBlcok> m_extruder_blocks; std::vector<ExtruderPreHeating::ExtruderUsageBlcok> m_extruder_blocks;
unsigned int m_machine_start_gcode_end_line_id{ (unsigned int) (-1) }; unsigned int m_machine_start_gcode_end_line_id{ (unsigned int) (-1) };
unsigned int m_machine_end_gcode_start_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 // 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 // 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 // (the fallback branch does occupancy bookkeeping only); read by the richer change-time model
+10
View File
@@ -1091,6 +1091,16 @@ namespace Slic3r
if (layer + 1 < layer_filaments.size()) next_lf = layer_filaments[layer + 1]; 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); 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; bool use_forcast = false;
float tmp_cost = 0; float tmp_cost = 0;
std::vector<unsigned int> sequence; std::vector<unsigned int> sequence;
+197 -19
View File
@@ -1609,6 +1609,64 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config)
if (max_vol_speed!= 0.f) if (max_vol_speed!= 0.f)
m_filpar[idx].max_e_speed = (max_vol_speed / filament_area()); 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_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; m_nozzle_change_perimeter_width = 2*m_perimeter_width;
// BBS: remove useless config // 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_initial_tool(m_current_tool)
.set_extrusion_flow(m_extrusion_flow) .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)) .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, 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)); (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.start_pos = writer.start_pos_rotated();
result.end_pos = writer.pos(); 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; nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width;
depth += nozzle_change_depth; 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); 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; tool_change.nozzle_change_depth = nozzle_change_depth;
m_plan.back().tool_changes.push_back(tool_change); 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"; 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 // BBS: consider both soluable and support properties
// Return index of first toolchange that switches to non-soluble and non-support extruder // Return index of first toolchange that switches to non-soluble and non-support extruder
// ot -1 if there is no such toolchange. // 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)) { && 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); 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; size_t old_tool = m_current_tool;
float wipe_depth = 0.f; 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 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; bool extruder_change = !is_in_same_extruder(old_filament_id, new_filament_id);
nozzle_change_speed = solid_infill ? 40.f * 60.f : nozzle_change_speed;//If the contact layers belong to different categories, then reduce the speed. 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)) { if (is_tpu_filament(m_current_tool)) {
nozzle_change_speed *= 0.25; 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); WipeTowerWriter writer(m_layer_height, m_nozzle_change_perimeter_width, m_gcode_flavor, m_filpar);
writer.set_extrusion_flow(nz_extrusion_flow) writer.set_extrusion_flow(nz_extrusion_flow)
.set_z(m_z_pos) .set_z(m_z_pos)
.set_initial_tool(m_current_tool) .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)) .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); WipeTowerBlock* block = get_block_by_category(m_filpar[old_filament_id].category, false);
if (!block) { 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; dy = solid_infill ? m_nozzle_change_perimeter_width : dy;
nozzle_change_line_count = solid_infill ? std::numeric_limits<int>::max() : nozzle_change_line_count; nozzle_change_line_count = solid_infill ? std::numeric_limits<int>::max() : nozzle_change_line_count;
m_left_to_right = true; 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; int real_nozzle_change_line_count = 0;
bool need_change_flow = false; bool need_change_flow = false;
for (int i = 0; true; ++i) { 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; block->last_nozzle_change_id = old_filament_id;
NozzleChangeResult result; 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; 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; nozzle_change_speed *= 2;
writer.travel(writer.x(), writer.y() - m_nozzle_change_perimeter_width); 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.start_pos = writer.start_pos_rotated();
result.origin_start_pos = initial_position; result.origin_start_pos = initial_position;
result.end_pos = writer.pos_rotated(); result.end_pos = writer.pos_rotated();
result.gcode = writer.gcode(); result.gcode = writer.gcode();
result.is_extruder_change = extruder_change;
return result; 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>] // 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 // 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 // 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 // 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 // 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 // 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 // only; every other printer's wipe tower is untouched.
// a preceding M400 wait is subsumed. // 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 = "") { auto format_line_M104 = [this](int target_temp, int target_extruder = -1, bool wait_for_moves = true, const std::string &comment = "") {
std::string buffer; std::string buffer;
buffer += "M632 S" + std::to_string(m_current_tool) + " M N\n"; if (wait_for_moves)
buffer += "M400\n";
buffer += "M104"; buffer += "M104";
if (target_extruder != -1 && target_extruder < (int) m_physical_extruder_map.size()) if (target_extruder != -1 && target_extruder < (int) m_physical_extruder_map.size())
buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder])); 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 buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by the slicer
if (!comment.empty()) buffer += " ;" + comment; if (!comment.empty()) buffer += " ;" + comment;
buffer += '\n'; buffer += '\n';
buffer += "M633\n";
(void) wait_for_moves; // the M632 M-flag barrier replaces the former M400 wait
return buffer; return buffer;
}; };
// Suppress the pre-heat M104 on the first layer and on solid (contact) toolchanges (should_heating). // m_is_multiple_nozzle gate needed because Orca calls toolchange_wipe_new for ALL printers (BBS has it H2C-only).
// 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.
bool should_heating = m_is_multiple_nozzle && m_filpar[m_current_tool].filament_cooling_before_tower > EPSILON && bool should_heating = m_is_multiple_nozzle && m_filpar[m_current_tool].filament_cooling_before_tower > EPSILON &&
!solid_tool_toolchange && !is_first_layer(); !solid_tool_toolchange && !is_first_layer();
auto add_M104_by_requirement = [&writer, &format_line_M104, &should_heating, this]() { 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]; 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, // 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? // 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), // 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; nozzle_change_depth = nozzle_change_line_count * m_nozzle_change_perimeter_width;
depth += nozzle_change_depth; 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.nozzle_change_depth = nozzle_change_depth;
toolchange.required_depth = depth; toolchange.required_depth = depth;
} }
+13
View File
@@ -58,6 +58,7 @@ public:
Vec2f origin_start_pos; // not rotated Vec2f origin_start_pos; // not rotated
std::vector<Vec2f> wipe_path; std::vector<Vec2f> wipe_path;
bool is_extruder_change{true};
}; };
struct ToolChangeResult struct ToolChangeResult
@@ -309,6 +310,8 @@ public:
int get_number_of_toolchanges() const { return m_num_tool_changes; } 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; } 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; } void set_has_tpu_filament(bool has_tpu) { m_has_tpu_filament = has_tpu; }
bool has_tpu_filament() const { return m_has_tpu_filament; } 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 // 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). // tower is reached; drives the prime-tower heating-during-wipe model (multi-nozzle only).
float filament_cooling_before_tower = 0.f; 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); 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); int get_filament_category(int filament_id);
bool is_in_same_extruder(int filament_id_1, int filament_id_2); 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(); void reset_block_status();
int get_wall_filament_for_all_layer(); int get_wall_filament_for_all_layer();
// for generate new wipe tower // for generate new wipe tower
@@ -453,6 +464,7 @@ private:
size_t m_cur_layer_id; size_t m_cur_layer_id;
NozzleChangeResult m_nozzle_change_result; NozzleChangeResult m_nozzle_change_result;
std::vector<int> m_filament_map; 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_has_tpu_filament{false};
bool m_is_multi_extruder{false}; bool m_is_multi_extruder{false};
bool m_use_gap_wall{false}; bool m_use_gap_wall{false};
@@ -555,6 +567,7 @@ private:
bool is_tpu_filament(int filament_id) const; bool is_tpu_filament(int filament_id) const;
bool is_petg_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 // BBS
box_coordinates align_perimeter(const box_coordinates& perimeter_box); box_coordinates align_perimeter(const box_coordinates& perimeter_box);
+97 -4
View File
@@ -2,6 +2,8 @@
#include "CustomGCode.hpp" #include "CustomGCode.hpp"
#include "I18N.hpp" #include "I18N.hpp"
#include "PrintConfig.hpp" #include "PrintConfig.hpp"
#include "ClipperUtils.hpp"
#include "Line.hpp"
#include <algorithm> #include <algorithm>
#include <iomanip> #include <iomanip>
#include <iostream> #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_z = LIMITS(machine_max_jerk_z);
m_max_jerk_e = LIMITS(machine_max_jerk_e); m_max_jerk_e = LIMITS(machine_max_jerk_e);
m_resolution = print_config.resolution.value; m_resolution = print_config.resolution.value;
#undef LIMITS #undef LIMITS
#undef LIMITS_UINT #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 &center, 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) 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 // 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()) { if (type == LiftType::SpiralLift && this->is_current_position_clear()) {
double radius = target_lift / (2 * PI * atan(filament()->travel_slope())); double radius = target_lift / (2 * PI * atan(filament()->travel_slope()));
// static spiral alignment when no move in x,y plane. // 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 }; 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"); 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 //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())); double radius = delta(2) / (2 * PI * atan(this->filament()->travel_slope()));
Vec2d ij_offset = radius * delta_no_z.normalized(); Vec2d ij_offset = radius * delta_no_z.normalized();
ij_offset = { -ij_offset(1), ij_offset(0) }; 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 //BBS: SlopeLift
else if (m_to_lift_type == LiftType::SlopeLift && else if (m_to_lift_type == LiftType::SlopeLift &&
+13
View File
@@ -6,6 +6,7 @@
#include <charconv> #include <charconv>
#include "Extruder.hpp" #include "Extruder.hpp"
#include "Point.hpp" #include "Point.hpp"
#include "Polygon.hpp"
#include "PrintConfig.hpp" #include "PrintConfig.hpp"
#include "GCode/CoolingBuffer.hpp" #include "GCode/CoolingBuffer.hpp"
@@ -181,6 +182,14 @@ public:
// Orca: slicing resolution in mm // Orca: slicing resolution in mm
double m_resolution = 0.01; 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_start;
std::string m_gcode_label_objects_end; std::string m_gcode_label_objects_end;
@@ -197,6 +206,10 @@ public:
std::string _travel_to_z(double z, const std::string &comment); 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); 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 &center, double radius) const;
std::string _retract(double length, double restart_extra, const std::string &comment); std::string _retract(double length, double restart_extra, const std::string &comment);
std::string set_acceleration_internal(Acceleration type, unsigned int acceleration); std::string set_acceleration_internal(Acceleration type, unsigned int acceleration);
+1 -1
View File
@@ -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_ramming_travel_time", "filament_ramming_travel_time_nc",
"filament_pre_cooling_temperature", "filament_pre_cooling_temperature_nc", "filament_pre_cooling_temperature", "filament_pre_cooling_temperature_nc",
"filament_preheat_temperature_delta", "filament_retract_length_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", "long_retractions_when_ec", "retraction_distances_when_ec",
"plugin_config_overrides", "plugin_config_overrides",
//ams chamber //ams chamber
+86 -19
View File
@@ -3605,7 +3605,8 @@ std::vector<std::set<int>> Print::get_physical_unprintable_filaments(const std::
return physical_unprintables; return physical_unprintables;
auto get_unprintable_extruder_id = [&](unsigned int filament_idx) -> int { 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) { for (int i = 0; i < extruder_num; ++i) {
if (!(status >> i & 1)) { if (!(status >> i & 1)) {
return i; 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()); 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_has_tpu_filament(this->has_tpu_filament());
wipe_tower.set_filament_map(this->get_filament_maps()); 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 // 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 // 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. // 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); 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(); 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(); unsigned int current_filament_id = m_wipe_tower_data.tool_ordering.first_extruder();
size_t cur_nozzle_id = filament_maps[current_filament_id] - 1; // Initialize NozzleStatusRecorder with the first filament's carousel slot
nozzle_cur_filament_ids[cur_nozzle_id] = current_filament_id; 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 for (auto& layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { // for all layers
++layer_idx;
if (!layer_tools.has_wipe_tower) continue; if (!layer_tools.has_wipe_tower) continue;
bool first_layer = &layer_tools == &m_wipe_tower_data.tool_ordering.front(); 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); 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) if (filament_id == current_filament_id)
continue; 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; 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]; // Per-carousel-slot purge tracking via NozzleStatusRecorder
// Fast purge mode uses flush_multiplier_fast; Default is inert. if (group_result) {
float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast) ? m_config.flush_multiplier_fast.get_at(nozzle_id) auto nozzle_info = group_result->get_nozzle_for_filament(filament_id, layer_idx);
: m_config.flush_multiplier.get_at(nozzle_id); if (nozzle_info) {
volume_to_purge *= flush_multiplier; int extruder_id = nozzle_info->extruder_id;
volume_to_purge = pre_filament_id == -1 ? 0 : int nozzle_id = nozzle_info->group_id;
layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_filament_id, filament_id, volume_to_purge); 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. //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); 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. // Select prime volume per-filament: nozzle change (carousel rotation) uses
float prime_volume = (m_config.prime_volume_mode == PrimeVolumeMode::pvmSaving) ? 15.f : (float) m_config.prime_volume; // 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, 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); prime_volume, volume_to_purge);
current_filament_id = filament_id; current_filament_id = filament_id;
nozzle_cur_filament_ids[nozzle_id] = filament_id;
} }
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
+12 -2
View File
@@ -7384,7 +7384,7 @@ void PrintConfigDef::init_fff_params()
def->tooltip = L("The flush multiplier used in fast purge mode."); def->tooltip = L("The flush multiplier used in fast purge mode.");
def->set_default_value(new ConfigOptionFloats{1.2}); 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 = this->add("prime_volume", coFloat);
def->label = L("Prime volume"); def->label = L("Prime volume");
def->tooltip = L("This is the volume of material to prime the extruder with on the tower."); 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->mode = comDevelop;
def->set_default_value(new ConfigOptionBool(false)); 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 = this->add("filament_prime_volume_nc", coFloats);
def->label = L("Hotend change"); def->label = L("Hotend change");
def->tooltip = L("The volume of material required to prime the extruder for a hotend change on the tower."); 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", "retraction_distance_when_cut",
"internal_bridge_support_thickness", "top_area_threshold", "reduce_wall_solid_infill","filament_load_time","filament_unload_time", "internal_bridge_support_thickness", "top_area_threshold", "reduce_wall_solid_infill","filament_load_time","filament_unload_time",
"smooth_coefficient", "overhang_totally_speed", "silent_mode", "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 "anisotropic_surfaces", // superseded by top_surface_fill_order / bottom_surface_fill_order
}; };
+1
View File
@@ -1841,6 +1841,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
// BBS: wipe tower is only used for priming // BBS: wipe tower is only used for priming
((ConfigOptionFloat, prime_volume)) ((ConfigOptionFloat, prime_volume))
// Nozzle-change (nc) prime volume + pre-heat delta // Nozzle-change (nc) prime volume + pre-heat delta
((ConfigOptionFloats, filament_prime_volume))
((ConfigOptionFloats, filament_prime_volume_nc)) ((ConfigOptionFloats, filament_prime_volume_nc))
((ConfigOptionFloatsNullable, filament_preheat_temperature_delta)) ((ConfigOptionFloatsNullable, filament_preheat_temperature_delta))
((ConfigOptionFloats, flush_multiplier)) ((ConfigOptionFloats, flush_multiplier))
+1
View File
@@ -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 << m_writer.set_pressure_advance(m_params.start);
gcode << "; end pressure advance pattern for layer\n"; gcode << "; end pressure advance pattern for layer\n";
+4 -4
View File
@@ -35,10 +35,10 @@ struct Calib_Params
{ {
Calib_Params() : mode(CalibMode::Calib_None){}; Calib_Params() : mode(CalibMode::Calib_None){};
int extruder_id = 0; int extruder_id = 0;
double start, end, step; double start = 0.0, end = 1.0, step = 0.1;
bool print_numbers; bool print_numbers = false;
double freqStartX, freqEndX, freqStartY, freqEndY; double freqStartX = 0.0, freqEndX = 1.0, freqStartY = 0.0, freqEndY = 1.0;
int test_model; int test_model = 0;
std::string shaper_type; std::string shaper_type;
std::vector<double> accelerations; std::vector<double> accelerations;
std::vector<double> speeds; std::vector<double> speeds;
+1
View File
@@ -558,6 +558,7 @@ namespace Slic3r
} }
else else
{ {
Slic3r::GUI::wxGetApp().reset_unsigned_plugin_warning();
if (m_agent) if (m_agent)
{ {
if (it->second->connection_type() != "lan" || it->second->connection_type().empty()) if (it->second->connection_type() != "lan" || it->second->connection_type().empty())
+16 -10
View File
@@ -1043,14 +1043,14 @@ void TextCtrl::propagate_value()
void TextCtrl::set_value(const boost::any& value, bool change_event/* = false*/) { void TextCtrl::set_value(const boost::any& value, bool change_event/* = false*/) {
m_disable_change_event = !change_event; m_disable_change_event = !change_event;
if (m_opt.nullable) { if (m_opt.nullable) {
const bool m_is_na_val = value.empty() || (boost::any_cast<wxString>(value) == _(L("N/A"))); const bool m_is_na_val = value.empty() || (boost::any_cast<wxString>(value) == _(L("N/A")));
if (!m_is_na_val) if (!m_is_na_val)
m_last_meaningful_value = value; 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; m_disable_change_event = false;
if (!change_event) { if (!change_event) {
@@ -1187,18 +1187,24 @@ void CheckBox::set_value(const boost::any& value, bool change_event)
m_disable_change_event = !change_event; m_disable_change_event = !change_event;
if (m_opt.nullable) { if (m_opt.nullable) {
const bool is_value_unsigned_char = value.type() == typeid(unsigned char); 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 && m_is_na_val = value.empty() || (is_value_unsigned_char &&
boost::any_cast<unsigned char>(value) == ConfigOptionBoolsNullable::nil_value()); 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 ? if (!m_is_na_val) {
boost::any_cast<unsigned char>(value) != 0 : bool_value = is_value_unsigned_char ?
boost::any_cast<bool>(value); boost::any_cast<unsigned char>(value) != 0 :
dynamic_cast<::CheckBox*>(window)->SetValue(m_is_na_val ? false : bool_value); // BBS 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)->SetValue(boost::any_cast<bool>(value)); // BBS
}
dynamic_cast<::CheckBox*>(window)->SetHalfChecked(value.empty()); dynamic_cast<::CheckBox*>(window)->SetHalfChecked(value.empty());
m_disable_change_event = false; m_disable_change_event = false;
} }
+17 -12
View File
@@ -6127,18 +6127,23 @@ bool GUI_App::process_network_msg(std::string dev_id, std::string msg)
} }
else if (msg == "unsigned_studio") { else if (msg == "unsigned_studio") {
BOOST_LOG_TRIVIAL(info) << "process_network_msg, unsigned_studio"; BOOST_LOG_TRIVIAL(info) << "process_network_msg, unsigned_studio";
MessageDialog // Plugin re-emits this on every subscribe retry; latch it so it shows
msg_dlg(nullptr, // once per connection episode.
_L("To use OrcaSlicer with Bambu Lab printers, you need to enable LAN mode and Developer mode on your printer.\n\n" if (!m_show_error_msgdlg && !m_unsigned_plugin_warning_shown) {
"Please go to your printer's settings and:\n" m_unsigned_plugin_warning_shown = true;
"1. Turn on LAN mode\n" MessageDialog
"2. Enable Developer mode\n\n" msg_dlg(nullptr,
"Developer mode allows the printer to work exclusively through local network access, " _L("To use OrcaSlicer with Bambu Lab printers, you need to enable LAN mode and Developer mode on your printer.\n\n"
"enabling full functionality with OrcaSlicer."), "Please go to your printer's settings and:\n"
_L("Network Plug-in Restriction"), wxAPPLY | wxOK); "1. Turn on LAN mode\n"
m_show_error_msgdlg = true; "2. Enable Developer mode\n\n"
msg_dlg.ShowModal(); "Developer mode allows the printer to work exclusively through local network access, "
m_show_error_msgdlg = false; "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; return true;
} }
} }
+1
View File
@@ -343,6 +343,7 @@ private:
public: public:
//try again when subscription fails //try again when subscription fails
void on_start_subscribe_again(std::string dev_id); 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(); std::string get_local_models_path();
bool OnInit() override; bool OnInit() override;
int OnExit() override; int OnExit() override;
+47 -6
View File
@@ -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; auto nozzle_volumes_values = preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")->values;
assert(obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2); assert(obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2);
if (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). // [Vortek] H2C: Use BBS-style NozzleGroupInfo comparison instead of direct nozzle type match.
NozzleVolumeType right_nozzle_type = DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0)); // This correctly handles Hybrid presets (which expand into per-type counts) and detects
NozzleVolumeType left_nozzle_type = DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(1)); // never-synced state (nozzle_count==0) so the first sync dialog appears.
NozzleVolumeType preset_left_type = NozzleVolumeType(nozzle_volumes_values[0]); // After device sync, extruder_nozzle_stats matches printer → dialog suppressed.
NozzleVolumeType preset_right_type = NozzleVolumeType(nozzle_volumes_values[1]); // Reference to BBS: BambuStudio/src/slic3r/GUI/Plater.cpp is_extruder_stat_synced()
is_same_as_printer = (left_nozzle_type == preset_left_type && right_nozzle_type == preset_right_type); 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; std::vector<std::map<int, int>> ams_count_info;
+13 -1
View File
@@ -3299,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) static std::vector<std::string> concat(std::vector<std::string> const& l, std::vector<std::string> const& r)
{ {
std::vector<std::string> t; 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; return t;
} }
+1 -1
View File
@@ -186,7 +186,7 @@ TroubleshootDialog::TroubleshootDialog()
Fit(); 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 ////////////////////// // RIGHT SIZER //////////////////////
+88
View File
@@ -0,0 +1,88 @@
# Compare Analyzer — G-code Slicing Comparison Tools
Tools for deep comparison and analysis of `.3mf` slicing project files, designed for
verifying multi-nozzle (H2C carousel) and multi-extruder slicing correctness.
## Tools
### `compare_slices.py` — Slice Comparison Analyzer
Deep comparison of two `.3mf` files (OrcaSlicer, BambuStudio, or any compatible slicer).
Generates a comprehensive Markdown report covering:
- **Filament usage** — per-filament weight/length with color mapping
- **Nozzle/extruder mapping** — Vortek carousel slot assignments
- **Tool change sequences** — T-code ordering and count
- **Prime tower analysis** — tower entries, G-code line count
- **Temperature timeline** — pre-heat lead times, target temperatures per tool change
- **Retract parameters** — M620.11 analysis during nozzle switches
- **Filament change G-code blocks** — line-by-line diff of change_filament_gcode
- **Control command diff** — timeline of M/G-code differences
- **Critical discrepancy detection** — automatic flagging of weight/time anomalies
#### Usage
```bash
# Compare two slice files
python3 compare_slices.py file1.3mf file2.3mf
# With custom labels
python3 compare_slices.py file1.3mf file2.3mf --labels "Upstream" "Fixed"
```
#### Output
Markdown report saved to `mp_reports/compare_report_YYYYMMDD_HHMMSS.md`
#### Example: Detecting H2C purge regression
```
⚠️ CRITICAL DISCREPANCY: Huge difference in part weight:
OrcaSlicer 60.90 g vs BambuStudio 17.47 g (difference 43.43 g or 71.3%).
The reason is incorrect nozzle mapping, causing huge AMS flushing.
```
---
### `show_temp_plot.py` — Temperature Timeline Plotter
Generates interactive HTML temperature plots for analyzing thermal profiles during
multi-nozzle prints. Visualizes heater temperature commands (M104/M109) per tool change,
showing pre-heat timing and temperature convergence.
#### Architecture
- H2C dual-extruder layout with Vortek carousel nozzles
- Physical heaters mapped dynamically:
- Heater 0: Extruder 2 (right nozzle slot, T0/T2/T3/T4)
- Heater 1: Extruder 1 (left nozzle slot, T1)
- Active heater mapping derived from G-code temperature signals
#### Usage
```bash
# Single file analysis
python3 show_temp_plot.py file.3mf
# Side-by-side comparison of two files
python3 show_temp_plot.py file1.3mf file2.3mf
```
#### Output
Interactive HTML report saved to Desktop as `temp_plot_v3.html`
---
## Requirements
- **Python 3.8+**
- **No external dependencies** — uses only Python standard library
(`json`, `zipfile`, `xml.etree.ElementTree`, `difflib`, `webbrowser`)
## Use Cases
1. **Regression testing** — compare slices before/after code changes to verify
no unintended differences in purge volumes, tool ordering, or temperature timing
2. **BBS compatibility verification** — compare OrcaSlicer output against BambuStudio
reference slices to ensure behavioral parity
3. **H2C carousel validation** — verify per-slot nozzle tracking produces correct
purge volumes (not collapsed per-extruder)
4. **Temperature protocol analysis** — verify pre-heat lead times and cooling
temperatures during nozzle changes match expected profiles
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+70
View File
@@ -1,5 +1,9 @@
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include <algorithm>
#include <sstream>
#include <string>
#include "libslic3r/calib.hpp" #include "libslic3r/calib.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp" #include "libslic3r/TriangleMesh.hpp"
@@ -38,3 +42,69 @@ TEST_CASE("Zero calibration line width resolves to a positive default", "[Calib]
REQUIRE(pattern.line_width() > 0.); REQUIRE(pattern.line_width() > 0.);
REQUIRE(pattern.line_width_first_layer() > 0.); REQUIRE(pattern.line_width_first_layer() > 0.);
} }
namespace {
struct EndState { double final_e; double max_e; };
EndState simulate_absolute_e(const std::string &gcode)
{
double final_e = 0.;
double max_e = 0.;
std::istringstream lines(gcode);
std::string line;
while (std::getline(lines, line)) {
std::istringstream words(line);
std::string op;
if (!(words >> op))
continue;
if (op != "G1" && op != "G0" && op != "G92")
continue;
std::string word;
while (words >> word) {
if (word.size() >= 2 && word[0] == 'E') {
final_e = std::stod(word.substr(1));
max_e = std::max(max_e, final_e);
break;
}
}
}
return {final_e, max_e};
}
} // namespace
TEST_CASE("PA pattern resets the extruder after the final layer in absolute E mode", "[Calib][Regression]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{"use_relative_e_distances", "0"},
{"line_width", "0.45"},
{"initial_layer_line_width", "0.45"},
});
Model model;
model.add_object("cube", "", make_cube(20, 20, 20))->add_instance();
Calib_Params params;
params.mode = CalibMode::Calib_PA_Pattern;
params.start = 0.;
params.end = 0.08;
params.step = 0.002;
CalibPressureAdvancePattern pattern(params, config, /* is_bbl_machine */ false, *model.objects.front(), Vec3d(0, 0, 0));
const CustomGCode::Info info = pattern.generate_custom_gcodes(config, /* is_bbl_machine */ false, *model.objects.front(),
Vec3d(0, 0, 0));
std::string gcode;
for (const CustomGCode::Item &item : info.gcodes)
gcode += item.extra;
const EndState state = simulate_absolute_e(gcode);
REQUIRE(state.max_e > 1.);
REQUIRE_THAT(state.final_e, Catch::Matchers::WithinAbs(0., 1e-9));
}