Merge main

This commit is contained in:
Lam Wei Lun
2026-08-21 12:28:24 +08:00
13 changed files with 500 additions and 53 deletions
+6
View File
@@ -856,6 +856,10 @@ std::string AppConfig::load()
local_machine.dev_ip = p["dev_ip"].get<std::string>(); local_machine.dev_ip = p["dev_ip"].get<std::string>();
if (p.contains("printer_type")) if (p.contains("printer_type"))
local_machine.printer_type = p["printer_type"].get<std::string>(); local_machine.printer_type = p["printer_type"].get<std::string>();
if (p.contains("printer_agent_id"))
local_machine.printer_agent_id = p["printer_agent_id"].get<std::string>();
if (p.contains("access_code"))
local_machine.access_code = p["access_code"].get<std::string>();
m_local_machines[local_machine.dev_id] = local_machine; m_local_machines[local_machine.dev_id] = local_machine;
} }
} else { } else {
@@ -1068,6 +1072,8 @@ void AppConfig::save()
m_json["dev_name"] = local_machine.second.dev_name; m_json["dev_name"] = local_machine.second.dev_name;
m_json["dev_ip"] = local_machine.second.dev_ip; m_json["dev_ip"] = local_machine.second.dev_ip;
m_json["printer_type"] = local_machine.second.printer_type; m_json["printer_type"] = local_machine.second.printer_type;
m_json["printer_agent_id"] = local_machine.second.printer_agent_id;
m_json["access_code"] = local_machine.second.access_code;
j["local_machines"][local_machine.first] = m_json; j["local_machines"][local_machine.first] = m_json;
} }
+10 -1
View File
@@ -66,10 +66,19 @@ struct BBLocalMachine
std::string dev_ip; std::string dev_ip;
std::string dev_id; /* serial number */ std::string dev_id; /* serial number */
std::string printer_type; /* model_id */ std::string printer_type; /* model_id */
std::string printer_agent_id; /* id of the IPrinterAgent that discovered/bound this device, e.g. "bbl"; empty for entries persisted before this field existed */
// Access code, scoped to printer_agent_id above - so a code saved while bound under one
// printer agent isn't treated as valid for a different, independent agent talking to the
// same physical dev_id. Empty for entries persisted before this field existed; those fall
// back to the legacy flat "access_code"/"user_access_code" AppConfig sections (BBL-only,
// since BBL was the only agent when they were saved) - see
// get_access_code_with_legacy_fallback() in DevManager.cpp.
std::string access_code;
bool operator==(const BBLocalMachine& other) const bool operator==(const BBLocalMachine& other) const
{ {
return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type; return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type &&
printer_agent_id == other.printer_agent_id && access_code == other.access_code;
} }
bool operator!=(const BBLocalMachine& other) const { return !operator==(other); } bool operator!=(const BBLocalMachine& other) const { return !operator==(other); }
}; };
+134 -20
View File
@@ -298,6 +298,7 @@ void GCodeProcessor::TimeMachine::State::reset()
//BBS //BBS
enter_direction = { 0.0f, 0.0f, 0.0f }; enter_direction = { 0.0f, 0.0f, 0.0f };
exit_direction = { 0.0f, 0.0f, 0.0f }; exit_direction = { 0.0f, 0.0f, 0.0f };
jd_unit_vec = { 0.0f, 0.0f, 0.0f, 0.0f };
} }
void GCodeProcessor::TimeMachine::CustomGCodeTime::reset() void GCodeProcessor::TimeMachine::CustomGCodeTime::reset()
@@ -5036,6 +5037,10 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
if (!is_extrusion_only_move(delta_pos)) if (!is_extrusion_only_move(delta_pos))
curr.enter_direction = curr.enter_direction / norm; curr.enter_direction = curr.enter_direction / norm;
curr.exit_direction = curr.enter_direction; curr.exit_direction = curr.enter_direction;
curr.jd_unit_vec = Vec4f(static_cast<float>(delta_pos[X]),
static_cast<float>(delta_pos[Y]),
static_cast<float>(delta_pos[Z]),
static_cast<float>(delta_pos[E])).normalized();
TimeBlock block; TimeBlock block;
block.move_type = type; block.move_type = type;
@@ -5118,22 +5123,32 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
block.acceleration = acceleration; block.acceleration = acceleration;
// calculates block exit feedrate static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
curr.safe_feedrate = block.feedrate_profile.cruise; const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
for (unsigned char a = X; a <= E; ++a) { // Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J).
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a)); // Negative leaves the classic jerk path below unchanged.
if (curr.abs_axis_feedrate[a] > axis_max_jerk) const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move,
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); static_cast<PrintEstimatedStatistics::ETimeMode>(i));
const bool use_junction_deviation = vmax_junction_jd >= 0.0f;
// calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is
// free to start from rest.
curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise;
if (!use_junction_deviation) {
for (unsigned char a = X; a <= E; ++a) {
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
}
} }
block.feedrate_profile.exit = curr.safe_feedrate; block.feedrate_profile.exit = curr.safe_feedrate;
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
// calculates block entry feedrate // calculates block entry feedrate
float vmax_junction = curr.safe_feedrate; float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate;
if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) { if (!use_junction_deviation && has_prev_move) {
bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise; bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise;
float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise); float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise);
// Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting. // Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting.
@@ -5400,6 +5415,10 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line)
if (!is_extrusion_only_move(delta_pos)) if (!is_extrusion_only_move(delta_pos))
curr.enter_direction = curr.enter_direction / norm; curr.enter_direction = curr.enter_direction / norm;
curr.exit_direction = curr.enter_direction; curr.exit_direction = curr.enter_direction;
curr.jd_unit_vec = Vec4f(static_cast<float>(delta_pos[X]),
static_cast<float>(delta_pos[Y]),
static_cast<float>(delta_pos[Z]),
static_cast<float>(delta_pos[E])).normalized();
TimeBlock block; TimeBlock block;
block.move_type = type; block.move_type = type;
@@ -5480,22 +5499,32 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line)
block.acceleration = acceleration; block.acceleration = acceleration;
// calculates block exit feedrate static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
curr.safe_feedrate = block.feedrate_profile.cruise; const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
for (unsigned char a = X; a <= E; ++a) { // Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J).
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a)); // Negative leaves the classic jerk path below unchanged.
if (curr.abs_axis_feedrate[a] > axis_max_jerk) const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move,
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); static_cast<PrintEstimatedStatistics::ETimeMode>(i));
const bool use_junction_deviation = vmax_junction_jd >= 0.0f;
// calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is
// free to start from rest.
curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise;
if (!use_junction_deviation) {
for (unsigned char a = X; a <= E; ++a) {
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
}
} }
block.feedrate_profile.exit = curr.safe_feedrate; block.feedrate_profile.exit = curr.safe_feedrate;
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
// calculates block entry feedrate // calculates block entry feedrate
float vmax_junction = curr.safe_feedrate; float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate;
if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) { if (!use_junction_deviation && has_prev_move) {
bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise; bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise;
float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise); float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise);
// Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting. // Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting.
@@ -7168,6 +7197,91 @@ float GCodeProcessor::get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeM
return get_axis_max_jerk_with_jd(mode, axis, get_acceleration(mode)); return get_axis_max_jerk_with_jd(mode, axis, get_acceleration(mode));
} }
float GCodeProcessor::get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const
{
const size_t id = static_cast<size_t>(mode);
// Klipper has no classic jerk: jd = scv^2 * (sqrt(2) - 1) / max_accel
// (toolhead.py::_calc_junction_deviation). Passing the block acceleration back in makes it cancel
// in calc_vmax_junction_deviation(), leaving the identity v == scv at a 90 degree corner.
if (m_flavor == gcfKlipper) {
// machine_max_jerk_x holds the square corner velocity; process_SET_VELOCITY_LIMIT() writes it.
const float scv = get_option_value(m_time_processor.machine_limits.machine_max_jerk_x, id);
if (scv <= 0.0f || acceleration <= 0.0f)
return 0.0f;
return sqr(scv) * (std::sqrt(2.0f) - 1.0f) / acceleration;
}
// Marlin 2 plans with junction deviation only when M205 J > 0; classic jerk leaves it at 0.
if (m_flavor == gcfMarlinFirmware)
return get_option_value(m_time_processor.machine_limits.machine_max_junction_deviation, id);
return 0.0f;
}
float GCodeProcessor::calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec,
PrintEstimatedStatistics::ETimeMode mode) const
{
float junction_acceleration = block.acceleration;
for (unsigned char a = X; a <= E; ++a) {
if (junction_unit_vec[a] == 0.0f)
continue;
const float axis_max_acceleration = get_axis_max_acceleration(mode, static_cast<Axis>(a), m_machine_config_idx);
if (axis_max_acceleration > 0.0f)
junction_acceleration = std::min(junction_acceleration, std::abs(axis_max_acceleration / junction_unit_vec[a]));
}
return junction_acceleration;
}
// Ported from PrusaSlicer (src/libslic3r/GCode/GCodeProcessor.cpp).
float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev,
const TimeMachine::State& curr, bool has_prev_move,
PrintEstimatedStatistics::ETimeMode mode) const
{
const float junction_deviation = get_junction_deviation(mode, block.acceleration);
if (junction_deviation <= 0.0f)
return -1.0f; // classic jerk machine, the caller keeps its own computation
if (!has_prev_move)
return 0.0f; // starts from rest, the planner raises this on the reverse pass
// -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin().
// Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance
// instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter
// than they are. Marlin normalizes over XYZE for any extruding move (planner.cpp, esteps > 0)
// and Klipper keeps E out of the cosine entirely (toolhead.py::Move.calc_junction); both agree
// that the corner is planned by its geometry, and normalizing matches them to within 1e-5.
float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec);
if (junction_cos_theta > 0.999999f)
return 0.0f; // the path doubles back, the machine has to stop
junction_cos_theta = std::max(junction_cos_theta, -0.999999f); // guards the division below
const float sin_theta_d2 = std::sqrt(0.5f * (1.0f - junction_cos_theta)); // always positive
const Vec4f junction_vec = curr.jd_unit_vec - prev.jd_unit_vec;
const float junction_vec_norm = junction_vec.norm();
const Vec4f junction_unit_vec = (junction_vec_norm > 0.0f) ? Vec4f(junction_vec / junction_vec_norm)
: Vec4f(0.0f, 0.0f, 0.0f, 0.0f);
const float junction_acceleration = calc_junction_acceleration(block, junction_unit_vec, mode);
float vmax_junction_sqr = (junction_acceleration * junction_deviation * sin_theta_d2) / (1.0f - sin_theta_d2);
// Marlin's JD_HANDLE_SMALL_SEGMENTS: a short move through a shallow corner is treated as an arc and
// capped by the centripetal acceleration it needs. Klipper has no equivalent.
if (m_flavor != gcfKlipper && block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
// Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin:
// https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html
const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f;
const float t = neg * junction_cos_theta;
const float asinx = 0.032843707f + t * (-1.451838349f + t * (29.66153956f + t * (-131.1123477f +
t * (262.8130562f + t * (-242.7199627f + t * (84.31466202f))))));
const float junction_theta = float(0.5 * M_PI) + neg * asinx; // acos(-t), bottoms out at 0.033
vmax_junction_sqr = std::min(vmax_junction_sqr, (block.distance * junction_acceleration) / junction_theta);
}
// Never faster than either of the two moves the junction joins.
vmax_junction_sqr = std::min(vmax_junction_sqr, std::min(sqr(block.feedrate_profile.cruise), sqr(prev.feedrate)));
return std::sqrt(vmax_junction_sqr);
}
float GCodeProcessor::get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const float GCodeProcessor::get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const
{ {
const size_t id = static_cast<size_t>(mode); const size_t id = static_cast<size_t>(mode);
+13
View File
@@ -637,6 +637,9 @@ class Print;
//For line move, there are same. For arc move, there are different. //For line move, there are same. For arc move, there are different.
Vec3f enter_direction; Vec3f enter_direction;
Vec3f exit_direction; Vec3f exit_direction;
// Orca: move direction over all four axes, unit length. Used by
// calc_vmax_junction_deviation(); see there for why E is normalized in.
Vec4f jd_unit_vec;
void reset(); void reset();
}; };
@@ -1488,6 +1491,16 @@ class Print;
float get_axis_max_acceleration(PrintEstimatedStatistics::ETimeMode mode, Axis axis, int machine_idx) const; float get_axis_max_acceleration(PrintEstimatedStatistics::ETimeMode mode, Axis axis, int machine_idx) const;
float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis, float acceleration) const; float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis, float acceleration) const;
float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const; float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
// Orca: junction deviation for a block at the given acceleration, 0 for a classic jerk machine.
float get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const;
// Orca: acceleration along the junction direction, clamped by the per axis limits.
float calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec,
PrintEstimatedStatistics::ETimeMode mode) const;
// Orca: entry speed from the junction deviation model, which limits a corner by its angle alone
// and is therefore isotropic, unlike per axis jerk. Negative means classic jerk applies instead.
float calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev,
const TimeMachine::State& curr, bool has_prev_move,
PrintEstimatedStatistics::ETimeMode mode) const;
float get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const; float get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
Vec3f get_xyz_max_jerk(PrintEstimatedStatistics::ETimeMode mode) const; Vec3f get_xyz_max_jerk(PrintEstimatedStatistics::ETimeMode mode) const;
float get_retract_acceleration(PrintEstimatedStatistics::ETimeMode mode) const; float get_retract_acceleration(PrintEstimatedStatistics::ETimeMode mode) const;
+1 -1
View File
@@ -5827,7 +5827,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() const {
Polygon PrintInstance::get_convex_hull_2d() { Polygon PrintInstance::get_convex_hull_2d() {
Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix()); Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix());
poly.douglas_peucker(0.1); poly.douglas_peucker(scale_(0.1));
return poly; return poly;
} }
+70 -21
View File
@@ -10,20 +10,36 @@
#include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/Plater.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Time.hpp" #include "libslic3r/Time.hpp"
using namespace nlohmann; using namespace nlohmann;
namespace { namespace {
// Orca: access_code and user_access_code used to be separate AppConfig keys before the two // Orca: access_code lives on BBLocalMachine::access_code (keyed by dev_id via
// fields were merged; fall back to the legacy key so existing users' saved codes aren't lost. // get_local_machines(), scoped by the record's own printer_agent_id field) - so binding a
std::string get_access_code_with_legacy_fallback(Slic3r::AppConfig* config, const std::string& dev_id) // printer under one agent doesn't silently appear as already-bound under a different,
// independent agent. This only covers LAN devices (BBLocalMachine's own scope); access_code
// and user_access_code used to be the only, flat dev_id-only AppConfig keys before
// BBLocalMachine::access_code existed, and codes saved back then are still stored flat (no
// agent association at all). Since BBL was the only agent that existed at the time, honor
// those flat legacy keys as implicitly BBL's - but only for the BBL agent, so they aren't
// leaked to other agents that never bound the device themselves.
std::string get_access_code_with_legacy_fallback(Slic3r::AppConfig* config, const std::string& dev_id, const std::string& agent_id)
{ {
std::string code = config->get("access_code", dev_id); const auto& machines = config->get_local_machines();
if (code.empty()) auto it = machines.find(dev_id);
code = config->get("user_access_code", dev_id); if (it != machines.end() && it->second.printer_agent_id == agent_id && !it->second.access_code.empty())
return code; return it->second.access_code;
if (agent_id == Slic3r::BBL_PRINTER_AGENT_ID || agent_id.empty()) {
std::string code = config->get("access_code", dev_id);
if (code.empty())
code = config->get("user_access_code", dev_id);
return code;
}
return "";
} }
} }
@@ -55,12 +71,13 @@ namespace Slic3r
continue; continue;
MachineObject* obj = new MachineObject(this, m_agent, m.dev_name, m.dev_id, m.dev_ip); MachineObject* obj = new MachineObject(this, m_agent, m.dev_name, m.dev_id, m.dev_ip);
obj->printer_type = m.printer_type; obj->printer_type = m.printer_type;
obj->printer_agent_id = m.printer_agent_id;
obj->dev_connection_type = "lan"; obj->dev_connection_type = "lan";
obj->bind_state = "free"; obj->bind_state = "free";
obj->bind_sec_link = "secure"; obj->bind_sec_link = "secure";
obj->m_is_online = true; obj->m_is_online = true;
obj->last_alive = Slic3r::Utils::get_current_time_utc(); obj->last_alive = Slic3r::Utils::get_current_time_utc();
obj->set_access_code(get_access_code_with_legacy_fallback(config, m.dev_id), false); obj->set_access_code(get_access_code_with_legacy_fallback(config, m.dev_id, obj->printer_agent_id), false);
if (obj->has_access_right()) { if (obj->has_access_right()) {
localMachineList.insert(std::make_pair(m.dev_id, obj)); localMachineList.insert(std::make_pair(m.dev_id, obj));
} else { } else {
@@ -77,10 +94,12 @@ namespace Slic3r
if (m.is_lan_mode_printer()) { if (m.is_lan_mode_printer()) {
if (m.has_access_right()) { if (m.has_access_right()) {
BBLocalMachine local_machine; BBLocalMachine local_machine;
local_machine.dev_id = m.get_dev_id(); local_machine.dev_id = m.get_dev_id();
local_machine.dev_name = m.get_dev_name(); local_machine.dev_name = m.get_dev_name();
local_machine.dev_ip = m.get_dev_ip(); local_machine.dev_ip = m.get_dev_ip();
local_machine.printer_type = m.printer_type; local_machine.printer_type = m.printer_type;
local_machine.printer_agent_id = m.printer_agent_id;
local_machine.access_code = m.get_access_code();
config->update_local_machine(local_machine); config->update_local_machine(local_machine);
} }
} else { } else {
@@ -143,6 +162,14 @@ namespace Slic3r
} }
} }
std::string DeviceManager::get_current_printer_agent_id() const
{
if (!m_agent)
return "";
auto printer_agent = m_agent->get_printer_agent();
return printer_agent ? printer_agent->get_agent_info().id : "";
}
void DeviceManager::EnableMultiMachine(bool enable) void DeviceManager::EnableMultiMachine(bool enable)
{ {
m_agent->enable_multi_machine(enable); m_agent->enable_multi_machine(enable);
@@ -339,6 +366,7 @@ namespace Slic3r
/* insert a new machine */ /* insert a new machine */
obj = new MachineObject(this, m_agent, dev_name, dev_id, dev_ip); obj = new MachineObject(this, m_agent, dev_name, dev_id, dev_ip);
obj->printer_type = _parse_printer_type(printer_type_str); obj->printer_type = _parse_printer_type(printer_type_str);
obj->printer_agent_id = get_current_printer_agent_id();
obj->wifi_signal = printer_signal; obj->wifi_signal = printer_signal;
obj->dev_connection_type = connect_type; obj->dev_connection_type = connect_type;
obj->bind_state = bind_state; obj->bind_state = bind_state;
@@ -350,7 +378,7 @@ namespace Slic3r
//load access code //load access code
AppConfig* config = Slic3r::GUI::wxGetApp().app_config; AppConfig* config = Slic3r::GUI::wxGetApp().app_config;
if (config) { if (config) {
obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id), false); obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id, obj->printer_agent_id), false);
} }
localMachineList.insert(std::make_pair(dev_id, obj)); localMachineList.insert(std::make_pair(dev_id, obj));
@@ -379,6 +407,7 @@ namespace Slic3r
obj = it->second; obj = it->second;
} else { } else {
obj = new MachineObject(this, m_agent, machine.dev_name, machine.dev_id, machine.dev_ip); obj = new MachineObject(this, m_agent, machine.dev_name, machine.dev_id, machine.dev_ip);
obj->printer_agent_id = get_current_printer_agent_id();
localMachineList.insert(std::make_pair(machine.dev_id, obj)); localMachineList.insert(std::make_pair(machine.dev_id, obj));
} }
if (machine.printer_type.empty()) if (machine.printer_type.empty())
@@ -505,16 +534,26 @@ namespace Slic3r
OnSelectedMachineChanged(previous_selected_machine, selected_machine); OnSelectedMachineChanged(previous_selected_machine, selected_machine);
} }
void DeviceManager::clear_other_devices() void DeviceManager::clear_other_devices(const std::string& target_agent_id)
{ {
// why: on agent swap, keep "My Devices" but drop the transient "Other Devices" // why: on agent swap, keep "My Devices" but drop the transient "Other Devices"
// Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own. // Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own.
//
// Also drop "My Devices" stamped by a different agent than the one we're swapping to
// (target_agent_id, passed by the caller since the live agent hasn't been repointed yet
// at this point): otherwise a device first discovered under agent A survives every swap
// with a stale printer_agent_id, stays hidden from every agent's filtered list, and only
// gets re-tagged if something happens to delete and re-create it (e.g. account logout).
// Dropping it here instead lets the new agent's start_discovery re-insert and re-stamp it
// like any other fresh device.
const auto my = get_my_machine_list(); const auto my = get_my_machine_list();
for (auto it = localMachineList.begin(); it != localMachineList.end();) for (auto it = localMachineList.begin(); it != localMachineList.end();)
{ {
if (my.find(it->first) == my.end()) const bool is_my_device = my.find(it->first) != my.end();
const bool agent_mismatch = !target_agent_id.empty() && it->second &&
it->second->printer_agent_id != target_agent_id;
if (!is_my_device || agent_mismatch)
{ {
// not a "My Device" -> an "Other Device"
delete it->second; delete it->second;
it = localMachineList.erase(it); it = localMachineList.erase(it);
} }
@@ -697,13 +736,16 @@ namespace Slic3r
m_agent->add_subscribe(subscribe_list_cache); m_agent->add_subscribe(subscribe_list_cache);
} }
std::map<std::string, MachineObject*> DeviceManager::get_my_machine_list() std::map<std::string, MachineObject*> DeviceManager::get_my_machine_list(const std::string& agent_id)
{ {
std::map<std::string, MachineObject*> result; std::map<std::string, MachineObject*> result;
for (auto it = userMachineList.begin(); it != userMachineList.end(); it++) for (auto it = userMachineList.begin(); it != userMachineList.end(); it++)
{ {
if (it->second && !it->second->is_lan_mode_printer()) if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id))
continue;
if (!it->second->is_lan_mode_printer())
{ {
result.insert(std::make_pair(it->first, it->second)); result.insert(std::make_pair(it->first, it->second));
} }
@@ -711,7 +753,10 @@ namespace Slic3r
for (auto it = localMachineList.begin(); it != localMachineList.end(); it++) for (auto it = localMachineList.begin(); it != localMachineList.end(); it++)
{ {
if (it->second && it->second->has_access_right() && it->second->is_avaliable() && it->second->is_lan_mode_printer()) if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id))
continue;
if (it->second->has_access_right() && it->second->is_avaliable() && it->second->is_lan_mode_printer())
{ {
// remove redundant in userMachineList // remove redundant in userMachineList
if (result.find(it->first) == result.end()) if (result.find(it->first) == result.end())
@@ -723,12 +768,15 @@ namespace Slic3r
return result; return result;
} }
std::map<std::string, MachineObject*> DeviceManager::get_my_cloud_machine_list() std::map<std::string, MachineObject*> DeviceManager::get_my_cloud_machine_list(const std::string& agent_id)
{ {
std::map<std::string, MachineObject*> result; std::map<std::string, MachineObject*> result;
for (auto it = userMachineList.begin(); it != userMachineList.end(); it++) for (auto it = userMachineList.begin(); it != userMachineList.end(); it++)
{ {
if (it->second && !it->second->is_lan_mode_printer()) { result.emplace(*it); } if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id))
continue;
if (!it->second->is_lan_mode_printer()) { result.emplace(*it); }
} }
return result; return result;
} }
@@ -801,6 +849,7 @@ namespace Slic3r
else else
{ {
obj = new MachineObject(this, m_agent, "", "", ""); obj = new MachineObject(this, m_agent, "", "", "");
obj->printer_agent_id = get_current_printer_agent_id();
if (m_agent) if (m_agent)
{ {
obj->set_bind_status(m_agent->get_user_name(provider)); obj->set_bind_status(m_agent->get_user_name(provider));
+11 -3
View File
@@ -74,7 +74,10 @@ public:
void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); } void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); }
void clean_user_info(bool keep_local_selection = false); void clean_user_info(bool keep_local_selection = false);
void clear_other_devices(); // target_agent_id: id of the agent being swapped to (empty = no agent-mismatch check,
// just the original "drop Other Devices" behavior). Pass the incoming agent's id, not the
// live one - this runs before the live agent is repointed.
void clear_other_devices(const std::string& target_agent_id = "");
void load_last_machine(); void load_last_machine();
void update_user_machine_list_info(const std::string& provider); void update_user_machine_list_info(const std::string& provider);
@@ -90,10 +93,15 @@ public:
/* my machine*/ /* my machine*/
MachineObject* get_my_machine(std::string dev_id); MachineObject* get_my_machine(std::string dev_id);
std::map<std::string, MachineObject*> get_my_machine_list(); std::map<std::string, MachineObject*> get_my_machine_list(const std::string& agent_id = "");
std::map<std::string, MachineObject*> get_my_cloud_machine_list(); std::map<std::string, MachineObject*> get_my_cloud_machine_list(const std::string& agent_id = "");
void modify_device_name(std::string dev_id, std::string dev_name, const std::string& provider); void modify_device_name(std::string dev_id, std::string dev_name, const std::string& provider);
// id of the currently live IPrinterAgent (IPrinterAgent::get_agent_info().id), or empty if
// m_agent has no printer agent set yet. Pass to get_my_machine_list()/get_my_cloud_machine_list()
// to scope results to the active agent.
std::string get_current_printer_agent_id() const;
/* create machine or update machine properties */ /* create machine or update machine properties */
void on_machine_alive(std::string json_str); void on_machine_alive(std::string json_str);
int query_bind_status(std::string& msg, const std::string& provider); int query_bind_status(std::string& msg, const std::string& provider);
+35 -4
View File
@@ -3,6 +3,7 @@
#include "libslic3r/Time.hpp" #include "libslic3r/Time.hpp"
#include "libslic3r/Thread.hpp" #include "libslic3r/Thread.hpp"
#include "slic3r/Utils/NetworkAgent.hpp" #include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "GuiColor.hpp" #include "GuiColor.hpp"
#include "GUI_App.hpp" #include "GUI_App.hpp"
@@ -458,11 +459,41 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
if (only_refresh) { if (only_refresh) {
AppConfig* config = GUI::wxGetApp().app_config; AppConfig* config = GUI::wxGetApp().app_config;
if (config) { if (config) {
if (!code.empty()) { if (is_lan_mode_printer()) {
GUI::wxGetApp().app_config->set_str("access_code", get_dev_id(), code); // why: LAN codes are scoped via BBLocalMachine::access_code, keyed by dev_id and
DeviceManager::update_local_machine(*this); // scoped by that record's own printer_agent_id field - see the matching comment
// on get_access_code_with_legacy_fallback() in DevManager.cpp - so binding this
// device under one printer agent doesn't silently read as already-bound under a
// different, independent one. Cloud devices (the else branch below) aren't
// scoped this way: they're never recalled from a stale local cache across a
// session boundary, since parse_user_print_info() always overwrites their code
// fresh from the cloud API's current response, so there's no cross-agent leakage
// risk to guard against there.
if (!code.empty()) {
DeviceManager::update_local_machine(*this);
} else {
// Only patch an existing record's code - don't persist a brand-new
// never-bound entry just because set_access_code("") was called on it.
const auto& machines = config->get_local_machines();
auto it = machines.find(get_dev_id());
if (it != machines.end()) {
BBLocalMachine local_machine = it->second;
local_machine.access_code = "";
config->update_local_machine(local_machine);
}
// Also clear the pre-scoping flat legacy key when unbinding under BBL, so an
// old BBL-era code can't silently "re-bind" this device again via
// get_access_code_with_legacy_fallback()'s legacy fallback.
if (printer_agent_id == BBL_PRINTER_AGENT_ID || printer_agent_id.empty()) {
config->erase("access_code", get_dev_id());
config->erase("user_access_code", get_dev_id());
}
}
} else { } else {
GUI::wxGetApp().app_config->erase("access_code", get_dev_id()); if (!code.empty())
config->set_str("access_code", get_dev_id(), code);
else
config->erase("access_code", get_dev_id());
} }
} }
} }
+10
View File
@@ -229,6 +229,16 @@ public:
//PRINTER_TYPE printer_type = PRINTER_3DPrinter_UKNOWN; //PRINTER_TYPE printer_type = PRINTER_3DPrinter_UKNOWN;
std::string printer_type; /* model_id */ std::string printer_type; /* model_id */
// id of the IPrinterAgent that was used to discover or bind this device (IPrinterAgent::get_agent_info().id,
// e.g. "bbl"), stamped at creation time — not derived from get_agent(), since m_agent is a single
// process-wide NetworkAgent shared by every MachineObject and gets repointed on agent swap
// (see DeviceManager::set_agent()), so it can't tell which agent originally found this device.
// We persist this as well so that when the printer agent is swapped, we don't show unrelated devices,
// e.g. if the current printer agent is elegoo, we shouldn't show printers connected by BBL printer agent
// under local machines.
std::string printer_agent_id;
std::string get_show_printer_type() const; std::string get_show_printer_type() const;
PrinterSeries get_printer_series() const; PrinterSeries get_printer_series() const;
PrinterArch get_printer_arch() const; PrinterArch get_printer_arch() const;
+7 -1
View File
@@ -3951,7 +3951,13 @@ void GUI_App::set_live_printer_agent(std::shared_ptr<IPrinterAgent> agent)
m_agent->set_user_selected_machine(""); m_agent->set_user_selected_machine("");
// note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer) // note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer)
dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS
dev->clear_other_devices(); // why: drop stale LAN discoveries; keep My Devices // why: drop stale LAN discoveries; keep My Devices, but only those belonging to the
// agent we're about to swap to, so a device stamped by the outgoing agent doesn't
// linger hidden - the new agent's start_discovery re-inserts and re-stamps it fresh.
// agent is null when clearing the live agent entirely (e.g. plugin unload); there's no
// target to filter against then, so fall back to the original "keep all My Devices"
// behavior rather than guessing.
dev->clear_other_devices(agent ? agent->get_agent_info().id : std::string());
} }
m_agent->set_printer_agent(agent); m_agent->set_printer_agent(agent);
+1 -1
View File
@@ -3912,7 +3912,7 @@ _collect_sorted_machines(Slic3r::DeviceManager* dev_manager,
}; };
// collect from user machine list // collect from user machine list
const auto& user_machine_list = dev_manager->get_my_machine_list();// user machine list const auto& user_machine_list = dev_manager->get_my_machine_list(dev_manager->get_current_printer_agent_id());// user machine list
for (const auto& elem : user_machine_list) for (const auto& elem : user_machine_list)
{ {
MachineObject* mobj = elem.second; MachineObject* mobj = elem.second;
+6 -1
View File
@@ -501,6 +501,7 @@ void SelectMachinePopup::update_other_devices()
DeviceManager* dev = wxGetApp().getDeviceManager(); DeviceManager* dev = wxGetApp().getDeviceManager();
if (!dev) return; if (!dev) return;
m_free_machine_list = dev->get_local_machinelist(); m_free_machine_list = dev->get_local_machinelist();
const std::string current_agent_id = dev->get_current_printer_agent_id();
BOOST_LOG_TRIVIAL(trace) << "SelectMachinePopup update_other_devices start"; BOOST_LOG_TRIVIAL(trace) << "SelectMachinePopup update_other_devices start";
this->Freeze(); this->Freeze();
@@ -512,6 +513,10 @@ void SelectMachinePopup::update_other_devices()
/* do not show printer bind state is empty */ /* do not show printer bind state is empty */
if (!mobj->is_avaliable()) continue; if (!mobj->is_avaliable()) continue;
/* do not show devices discovered/bound by a different printer agent */
if (mobj->printer_agent_id != current_agent_id)
continue;
if (!wxGetApp().is_user_login(wxGetApp().get_printer_cloud_provider()) && !mobj->is_lan_mode_printer()) if (!wxGetApp().is_user_login(wxGetApp().get_printer_cloud_provider()) && !mobj->is_lan_mode_printer())
continue; continue;
@@ -634,7 +639,7 @@ void SelectMachinePopup::update_user_devices()
} }
m_bind_machine_list.clear(); m_bind_machine_list.clear();
m_bind_machine_list = dev->get_my_machine_list(); m_bind_machine_list = dev->get_my_machine_list(dev->get_current_printer_agent_id());
//sort list //sort list
std::vector<std::pair<std::string, MachineObject*>> user_machine_list; std::vector<std::pair<std::string, MachineObject*>> user_machine_list;
+196
View File
@@ -7,9 +7,14 @@
#include "test_utils.hpp" #include "test_utils.hpp"
#include <cmath>
#include <fstream> #include <fstream>
#include <iomanip>
#include <map> #include <map>
#include <memory> #include <memory>
#include <sstream>
#include <string>
#include <vector>
using namespace Slic3r; using namespace Slic3r;
using Catch::Matchers::WithinAbs; using Catch::Matchers::WithinAbs;
@@ -418,3 +423,194 @@ TEST_CASE("Per-slot machine limits follow the active nozzle", "[GCodeTiming][Mul
REQUIRE_THAT(times[2], Catch::Matchers::WithinRel(101.0 / 200.0, 0.10)); REQUIRE_THAT(times[2], Catch::Matchers::WithinRel(101.0 / 200.0, 0.10));
} }
} }
// Junction planning decides the speeds the "actual speed" / "actual flow" preview shows. Per-axis
// jerk limits a corner by the largest single-axis component of the velocity change, allowing sqrt(2)
// more speed on a diagonal than on an axis -- a four-lobed ripple around every circle. Klipper and
// Marlin 2 with M205 J plan with junction deviation instead, which sees only the corner angle.
namespace {
// One acceleration everywhere and axis limits far above it, so only the junction model under test
// can slow a corner down.
FullPrintConfig make_junction_config(GCodeFlavor flavor, double corner_velocity, double junction_deviation)
{
FullPrintConfig config;
config.gcode_flavor.value = flavor;
config.filament_diameter.values = {1.75};
config.filament_map.values = {1};
const std::vector<double> accel = {1000.0, 1000.0};
const std::vector<double> axis = {20000.0, 20000.0};
const std::vector<double> speed = {500.0, 500.0};
config.machine_max_acceleration_extruding.values = accel;
config.machine_max_acceleration_travel.values = accel;
config.machine_max_acceleration_retracting.values = accel;
config.machine_max_acceleration_x.values = axis;
config.machine_max_acceleration_y.values = axis;
config.machine_max_acceleration_z.values = axis;
config.machine_max_acceleration_e.values = axis;
config.machine_max_speed_x.values = speed;
config.machine_max_speed_y.values = speed;
config.machine_max_speed_z.values = speed;
config.machine_max_speed_e.values = speed;
// Klipper reads this as the square corner velocity, Marlin as classic jerk.
config.machine_max_jerk_x.values = {corner_velocity, corner_velocity};
config.machine_max_jerk_y.values = {corner_velocity, corner_velocity};
config.machine_max_jerk_z.values = {corner_velocity, corner_velocity};
// Kept out of the way so it never binds in the classic-jerk comparisons.
config.machine_max_jerk_e.values = {100.0, 100.0};
config.machine_max_junction_deviation.values = {junction_deviation, junction_deviation};
config.machine_min_extruding_rate.values = {0.0, 0.0};
config.machine_min_travel_rate.values = {0.0, 0.0};
return config;
}
constexpr double junction_x = 60.0;
constexpr double junction_y = 60.0;
// Two 40mm moves meeting at (junction_x, junction_y) with the given turn, rotated by `orientation`.
// 40mm is long enough to reach the commanded 150mm/s and brake back to any corner speed these tests
// produce. `e_per_mm` of zero makes them travels, which keeps the junction vector purely geometric
// as the formulas below assume.
std::string corner_gcode(double turn_deg, double orientation_deg, double e_per_mm = 0.0)
{
const double len = 40.0;
const double a_in = orientation_deg * M_PI / 180.0;
const double a_out = (orientation_deg + turn_deg) * M_PI / 180.0;
std::ostringstream extrude;
if (e_per_mm > 0.0)
extrude << std::fixed << std::setprecision(4) << " E" << len * e_per_mm;
std::ostringstream os;
os << std::fixed << std::setprecision(4)
<< "M83\n"
<< "G1 Z0.2 F1200\n"
<< "G1 X" << junction_x - len * std::cos(a_in) << " Y" << junction_y - len * std::sin(a_in) << " F6000\n"
<< "G1 X" << junction_x << " Y" << junction_y << extrude.str() << " F9000\n"
<< "G1 X" << junction_x + len * std::cos(a_out) << " Y" << junction_y + len * std::sin(a_out)
<< extrude.str() << " F9000\n";
return os.str();
}
// Speed allowed through the corner: the vertex ending the incoming move carries that block's exit
// speed, and the vertices the actual-speed pass inserts are all strictly interior.
double corner_speed(const GCodeProcessorResult& r)
{
for (const auto& mv : r.moves)
if ((mv.type == EMoveType::Travel || mv.type == EMoveType::Extrude) &&
std::abs(mv.position.x() - junction_x) < 1e-3 &&
std::abs(mv.position.y() - junction_y) < 1e-3)
return mv.actual_feedrate;
return -1.0;
}
double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double junction_deviation,
double turn_deg, double orientation_deg = 0.0, double e_per_mm = 0.0)
{
GCodeProcessor proc;
run_processor(proc, make_junction_config(flavor, corner_velocity, junction_deviation),
corner_gcode(turn_deg, orientation_deg, e_per_mm).c_str());
return corner_speed(proc.get_result());
}
} // namespace
TEST_CASE("Klipper corners are planned with junction deviation derived from the square corner velocity",
"[GCodeTiming][JunctionDeviation]")
{
// jd = scv^2 * (sqrt(2) - 1) / max_accel, then v^2 = jd * accel * sin(t/2) / (1 - sin(t/2)).
// The acceleration cancels: the corner speed depends only on the scv and the angle.
const double scv = 5.0;
SECTION("a right angle is taken at exactly the square corner velocity") {
// sin(t/2) = sqrt(0.5) at 90 degrees, so v == scv -- the definition of the square corner
// velocity, and what makes the mapping above the right one.
REQUIRE_THAT(planned_corner_speed(gcfKlipper, scv, 0.0, 90.0), Catch::Matchers::WithinRel(scv, 0.02));
}
SECTION("a shallow corner is taken far faster than the per-axis jerk model allows") {
// 6 degrees: sin(t/2) = cos(3 deg), so v = 5 * sqrt((sqrt(2) - 1) * 728.68) = 86.9mm/s. Per-axis
// jerk ignores the angle and caps the velocity *change* (2v*sin(3 deg)), giving 47.8mm/s.
const double jd_speed = planned_corner_speed(gcfKlipper, scv, 0.0, 6.0);
const double jerk_speed = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, 6.0);
REQUIRE_THAT(jd_speed, Catch::Matchers::WithinRel(86.87, 0.02));
REQUIRE_THAT(jerk_speed, Catch::Matchers::WithinRel(47.75, 0.02));
}
}
TEST_CASE("Junction deviation limits a corner by its angle alone, not by its orientation",
"[GCodeTiming][JunctionDeviation]")
{
// The four-lobed ripple on circular walls is per-axis jerk being anisotropic: a velocity change
// lying on an axis gets sqrt(2) less headroom than the same change on the diagonal.
const double scv = 5.0;
const double turn = 6.0;
SECTION("Klipper plans both orientations identically") {
const double on_axis = planned_corner_speed(gcfKlipper, scv, 0.0, turn, 0.0);
const double diagonal = planned_corner_speed(gcfKlipper, scv, 0.0, turn, 45.0);
REQUIRE(on_axis > 0.0);
REQUIRE_THAT(diagonal, Catch::Matchers::WithinRel(on_axis, 0.02));
}
SECTION("the classic jerk model keeps its orientation dependence") {
const double on_axis = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, turn, 0.0);
const double diagonal = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, turn, 45.0);
REQUIRE(on_axis > 0.0);
REQUIRE(diagonal / on_axis > 1.2);
}
}
TEST_CASE("Junction deviation is only used where the firmware actually plans with it",
"[GCodeTiming][JunctionDeviation]")
{
const double jerk = 5.0;
SECTION("Marlin 2 with M205 J disabled keeps the classic jerk planning") {
// machine_max_junction_deviation == 0 is how a Marlin 2 printer says it runs classic jerk.
const double classic = planned_corner_speed(gcfMarlinLegacy, jerk, 0.0, 90.0);
REQUIRE(classic > 0.0);
REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, jerk, 0.0, 90.0),
Catch::Matchers::WithinRel(classic, 1e-4));
}
SECTION("Marlin 2 with M205 J enabled switches to junction deviation") {
// sqrt(1000 * 0.05 * 2.4142136) = 11.0mm/s, independent of the jerk values it no longer reads.
REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, jerk, 0.05, 90.0),
Catch::Matchers::WithinRel(10.99, 0.02));
}
SECTION("machines without junction deviation are untouched by the jerk values it would ignore") {
// A flavor that never enters the junction deviation path must ignore the setting entirely.
const double without = planned_corner_speed(gcfMarlinLegacy, jerk, 0.0, 90.0);
REQUIRE_THAT(planned_corner_speed(gcfMarlinLegacy, jerk, 0.05, 90.0),
Catch::Matchers::WithinRel(without, 1e-4));
}
}
TEST_CASE("How fast a corner is taken does not depend on how much is extruded through it",
"[GCodeTiming][JunctionDeviation]")
{
// The junction cosine is taken over XYZE, so the direction vectors have to be unit length or the
// E term makes the two paths look more parallel than they are and the corner comes out too fast,
// the more so the higher the flow. Marlin normalizes over XYZE on any extruding move
// (planner.cpp, esteps > 0) and Klipper leaves E out of the cosine altogether
// (toolhead.py::Move.calc_junction); on both, this corner is planned by its geometry alone.
const double scv = 5.0;
const double turn = 6.0;
const double geometric = planned_corner_speed(gcfKlipper, scv, 0.0, turn);
REQUIRE(geometric > 0.0);
// 0.029mm/mm is an ordinary 0.42 x 0.2 line on 1.75mm filament; 0.1 is a fat large-nozzle one.
// Unnormalized these came out at 94.4 and 150.0mm/s against a geometric 86.9.
for (double e_per_mm : {0.029, 0.1})
REQUIRE_THAT(planned_corner_speed(gcfKlipper, scv, 0.0, turn, 0.0, e_per_mm),
Catch::Matchers::WithinRel(geometric, 0.02));
SECTION("and the same holds on Marlin 2") {
const double marlin = planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn);
REQUIRE(marlin > 0.0);
REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn, 0.0, 0.029),
Catch::Matchers::WithinRel(marlin, 0.02));
}
}