mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-23 11:39:37 +03:00
Compare commits
24 Commits
feat/plugi
...
feat/plate
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
790a010615 | ||
|
|
aaa8e98bb0 | ||
|
|
dc75ce6811 | ||
|
|
ba22973919 | ||
|
|
9d37ee4709 | ||
|
|
b10c91cf11 | ||
|
|
5d6008eed2 | ||
|
|
6345d57512 | ||
|
|
159e577543 | ||
|
|
b2e05d0683 | ||
|
|
ae27a09ffe | ||
|
|
cd33f589dc | ||
|
|
9ee736fe27 | ||
|
|
ced1058b31 | ||
|
|
aae83220f1 | ||
|
|
f3f44bffcb | ||
|
|
2d3e911efa | ||
|
|
56236f56a8 | ||
|
|
df5a08517a | ||
|
|
5d953f915a | ||
|
|
dd2cb92685 | ||
|
|
b2f08c3ff8 | ||
|
|
75a2460649 | ||
|
|
01493d4e3a |
@@ -856,6 +856,10 @@ std::string AppConfig::load()
|
||||
local_machine.dev_ip = p["dev_ip"].get<std::string>();
|
||||
if (p.contains("printer_type"))
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
@@ -1068,6 +1072,8 @@ void AppConfig::save()
|
||||
m_json["dev_name"] = local_machine.second.dev_name;
|
||||
m_json["dev_ip"] = local_machine.second.dev_ip;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -66,10 +66,19 @@ struct BBLocalMachine
|
||||
std::string dev_ip;
|
||||
std::string dev_id; /* serial number */
|
||||
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
|
||||
{
|
||||
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); }
|
||||
};
|
||||
|
||||
@@ -298,6 +298,7 @@ void GCodeProcessor::TimeMachine::State::reset()
|
||||
//BBS
|
||||
enter_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()
|
||||
@@ -5036,6 +5037,10 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
if (!is_extrusion_only_move(delta_pos))
|
||||
curr.enter_direction = curr.enter_direction / norm;
|
||||
curr.exit_direction = curr.enter_direction;
|
||||
curr.jd_unit_vec = Vec4f(static_cast<float>(delta_pos[X]) * inv_distance,
|
||||
static_cast<float>(delta_pos[Y]) * inv_distance,
|
||||
static_cast<float>(delta_pos[Z]) * inv_distance,
|
||||
static_cast<float>(delta_pos[E]) * inv_distance);
|
||||
|
||||
TimeBlock block;
|
||||
block.move_type = type;
|
||||
@@ -5118,22 +5123,32 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
|
||||
block.acceleration = acceleration;
|
||||
|
||||
// calculates block exit feedrate
|
||||
curr.safe_feedrate = block.feedrate_profile.cruise;
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
|
||||
|
||||
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);
|
||||
// Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J).
|
||||
// Negative leaves the classic jerk path below unchanged.
|
||||
const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move,
|
||||
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;
|
||||
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
|
||||
// calculates block entry feedrate
|
||||
float vmax_junction = curr.safe_feedrate;
|
||||
if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) {
|
||||
float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate;
|
||||
if (!use_junction_deviation && has_prev_move) {
|
||||
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);
|
||||
// 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))
|
||||
curr.enter_direction = curr.enter_direction / norm;
|
||||
curr.exit_direction = curr.enter_direction;
|
||||
curr.jd_unit_vec = Vec4f(static_cast<float>(delta_pos[X]) * inv_distance,
|
||||
static_cast<float>(delta_pos[Y]) * inv_distance,
|
||||
static_cast<float>(delta_pos[Z]) * inv_distance,
|
||||
static_cast<float>(delta_pos[E]) * inv_distance);
|
||||
|
||||
TimeBlock block;
|
||||
block.move_type = type;
|
||||
@@ -5480,22 +5499,32 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line)
|
||||
|
||||
block.acceleration = acceleration;
|
||||
|
||||
// calculates block exit feedrate
|
||||
curr.safe_feedrate = block.feedrate_profile.cruise;
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
|
||||
|
||||
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);
|
||||
// Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J).
|
||||
// Negative leaves the classic jerk path below unchanged.
|
||||
const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move,
|
||||
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;
|
||||
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
|
||||
// calculates block entry feedrate
|
||||
float vmax_junction = curr.safe_feedrate;
|
||||
if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) {
|
||||
float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate;
|
||||
if (!use_junction_deviation && has_prev_move) {
|
||||
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);
|
||||
// Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting.
|
||||
@@ -7168,6 +7197,86 @@ float GCodeProcessor::get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeM
|
||||
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().
|
||||
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
|
||||
{
|
||||
const size_t id = static_cast<size_t>(mode);
|
||||
|
||||
@@ -637,6 +637,10 @@ class Print;
|
||||
//For line move, there are same. For arc move, there are different.
|
||||
Vec3f enter_direction;
|
||||
Vec3f exit_direction;
|
||||
// Orca: move direction over all four axes, scaled by 1 / block.distance. Used by
|
||||
// calc_vmax_junction_deviation(), which needs E to see extrusion-rate changes
|
||||
// between collinear moves the way Marlin and Klipper do.
|
||||
Vec4f jd_unit_vec;
|
||||
|
||||
void reset();
|
||||
};
|
||||
@@ -1488,6 +1492,16 @@ class Print;
|
||||
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) 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;
|
||||
Vec3f get_xyz_max_jerk(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
float get_retract_acceleration(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
|
||||
@@ -559,11 +559,9 @@ static inline bool model_volume_solid_or_modifier(const ModelVolume &mv)
|
||||
|
||||
static inline Transform3f trafo_for_bbox(const Transform3d &object_trafo, const Transform3d &volume_trafo)
|
||||
{
|
||||
// Orca: Keep the volume's local XY offset for multipart overlap checks, but remove the object's bed placement.
|
||||
Transform3d object_trafo_local = object_trafo;
|
||||
object_trafo_local.translation().x() = 0.;
|
||||
object_trafo_local.translation().y() = 0.;
|
||||
Transform3d m = object_trafo_local * volume_trafo;
|
||||
Transform3d m = object_trafo * volume_trafo;
|
||||
m.translation().x() = 0.;
|
||||
m.translation().y() = 0.;
|
||||
return m.cast<float>();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,20 +10,36 @@
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
|
||||
#include "libslic3r/Time.hpp"
|
||||
|
||||
using namespace nlohmann;
|
||||
|
||||
namespace {
|
||||
// Orca: access_code and user_access_code used to be separate AppConfig keys before the two
|
||||
// fields were merged; fall back to the legacy key so existing users' saved codes aren't lost.
|
||||
std::string get_access_code_with_legacy_fallback(Slic3r::AppConfig* config, const std::string& dev_id)
|
||||
// Orca: access_code lives on BBLocalMachine::access_code (keyed by dev_id via
|
||||
// get_local_machines(), scoped by the record's own printer_agent_id field) - so binding a
|
||||
// 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);
|
||||
if (code.empty())
|
||||
code = config->get("user_access_code", dev_id);
|
||||
return code;
|
||||
const auto& machines = config->get_local_machines();
|
||||
auto it = machines.find(dev_id);
|
||||
if (it != machines.end() && it->second.printer_agent_id == agent_id && !it->second.access_code.empty())
|
||||
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;
|
||||
MachineObject* obj = new MachineObject(this, m_agent, m.dev_name, m.dev_id, m.dev_ip);
|
||||
obj->printer_type = m.printer_type;
|
||||
obj->printer_agent_id = m.printer_agent_id;
|
||||
obj->dev_connection_type = "lan";
|
||||
obj->bind_state = "free";
|
||||
obj->bind_sec_link = "secure";
|
||||
obj->m_is_online = true;
|
||||
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()) {
|
||||
localMachineList.insert(std::make_pair(m.dev_id, obj));
|
||||
} else {
|
||||
@@ -77,10 +94,12 @@ namespace Slic3r
|
||||
if (m.is_lan_mode_printer()) {
|
||||
if (m.has_access_right()) {
|
||||
BBLocalMachine local_machine;
|
||||
local_machine.dev_id = m.get_dev_id();
|
||||
local_machine.dev_name = m.get_dev_name();
|
||||
local_machine.dev_ip = m.get_dev_ip();
|
||||
local_machine.printer_type = m.printer_type;
|
||||
local_machine.dev_id = m.get_dev_id();
|
||||
local_machine.dev_name = m.get_dev_name();
|
||||
local_machine.dev_ip = m.get_dev_ip();
|
||||
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);
|
||||
}
|
||||
} 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)
|
||||
{
|
||||
m_agent->enable_multi_machine(enable);
|
||||
@@ -339,6 +366,7 @@ namespace Slic3r
|
||||
/* insert a new machine */
|
||||
obj = new MachineObject(this, m_agent, dev_name, dev_id, dev_ip);
|
||||
obj->printer_type = _parse_printer_type(printer_type_str);
|
||||
obj->printer_agent_id = get_current_printer_agent_id();
|
||||
obj->wifi_signal = printer_signal;
|
||||
obj->dev_connection_type = connect_type;
|
||||
obj->bind_state = bind_state;
|
||||
@@ -350,7 +378,7 @@ namespace Slic3r
|
||||
//load access code
|
||||
AppConfig* config = Slic3r::GUI::wxGetApp().app_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));
|
||||
|
||||
@@ -379,6 +407,7 @@ namespace Slic3r
|
||||
obj = it->second;
|
||||
} else {
|
||||
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));
|
||||
}
|
||||
if (machine.printer_type.empty())
|
||||
@@ -505,16 +534,26 @@ namespace Slic3r
|
||||
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"
|
||||
// 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();
|
||||
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;
|
||||
it = localMachineList.erase(it);
|
||||
}
|
||||
@@ -697,13 +736,16 @@ namespace Slic3r
|
||||
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;
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -711,7 +753,10 @@ namespace Slic3r
|
||||
|
||||
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
|
||||
if (result.find(it->first) == result.end())
|
||||
@@ -723,12 +768,15 @@ namespace Slic3r
|
||||
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;
|
||||
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;
|
||||
}
|
||||
@@ -801,6 +849,7 @@ namespace Slic3r
|
||||
else
|
||||
{
|
||||
obj = new MachineObject(this, m_agent, "", "", "");
|
||||
obj->printer_agent_id = get_current_printer_agent_id();
|
||||
if (m_agent)
|
||||
{
|
||||
obj->set_bind_status(m_agent->get_user_name(provider));
|
||||
|
||||
@@ -74,7 +74,10 @@ public:
|
||||
void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); }
|
||||
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 update_user_machine_list_info(const std::string& provider);
|
||||
@@ -90,10 +93,15 @@ public:
|
||||
|
||||
/* my machine*/
|
||||
MachineObject* get_my_machine(std::string dev_id);
|
||||
std::map<std::string, MachineObject*> get_my_machine_list();
|
||||
std::map<std::string, MachineObject*> get_my_cloud_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(const std::string& agent_id = "");
|
||||
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 */
|
||||
void on_machine_alive(std::string json_str);
|
||||
int query_bind_status(std::string& msg, const std::string& provider);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "libslic3r/Time.hpp"
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
#include "GuiColor.hpp"
|
||||
|
||||
#include "GUI_App.hpp"
|
||||
@@ -458,11 +459,41 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
|
||||
if (only_refresh) {
|
||||
AppConfig* config = GUI::wxGetApp().app_config;
|
||||
if (config) {
|
||||
if (!code.empty()) {
|
||||
GUI::wxGetApp().app_config->set_str("access_code", get_dev_id(), code);
|
||||
DeviceManager::update_local_machine(*this);
|
||||
if (is_lan_mode_printer()) {
|
||||
// why: LAN codes are scoped via BBLocalMachine::access_code, keyed by dev_id and
|
||||
// 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 {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,16 @@ public:
|
||||
|
||||
//PRINTER_TYPE printer_type = PRINTER_3DPrinter_UKNOWN;
|
||||
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;
|
||||
PrinterSeries get_printer_series() const;
|
||||
PrinterArch get_printer_arch() const;
|
||||
|
||||
@@ -3951,7 +3951,13 @@ void GUI_App::set_live_printer_agent(std::shared_ptr<IPrinterAgent> agent)
|
||||
m_agent->set_user_selected_machine("");
|
||||
// note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer)
|
||||
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);
|
||||
|
||||
@@ -3912,7 +3912,7 @@ _collect_sorted_machines(Slic3r::DeviceManager* dev_manager,
|
||||
};
|
||||
|
||||
// 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)
|
||||
{
|
||||
MachineObject* mobj = elem.second;
|
||||
|
||||
@@ -501,6 +501,7 @@ void SelectMachinePopup::update_other_devices()
|
||||
DeviceManager* dev = wxGetApp().getDeviceManager();
|
||||
if (!dev) return;
|
||||
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";
|
||||
this->Freeze();
|
||||
@@ -512,6 +513,10 @@ void SelectMachinePopup::update_other_devices()
|
||||
/* do not show printer bind state is empty */
|
||||
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())
|
||||
continue;
|
||||
|
||||
@@ -634,7 +639,7 @@ void SelectMachinePopup::update_user_devices()
|
||||
}
|
||||
|
||||
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
|
||||
std::vector<std::pair<std::string, MachineObject*>> user_machine_list;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <slic3r/GUI/MsgDialog.hpp>
|
||||
#include <slic3r/GUI/PluginProgressDialog.hpp>
|
||||
#include <slic3r/GUI/PluginWebDialog.hpp>
|
||||
#include <slic3r/GUI/NotificationManager.hpp>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <pybind11/pybind11.h>
|
||||
@@ -19,6 +20,7 @@
|
||||
#include <wx/defs.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
@@ -44,16 +46,20 @@ namespace {
|
||||
struct GilSafeCallable
|
||||
{
|
||||
py::object fn;
|
||||
std::atomic_bool active{true};
|
||||
explicit GilSafeCallable(py::object f) : fn(std::move(f)) {}
|
||||
void disable()
|
||||
{
|
||||
active.store(false, std::memory_order_release);
|
||||
PythonGILState gil;
|
||||
if (gil)
|
||||
fn = py::object();
|
||||
else
|
||||
(void) fn.release();
|
||||
}
|
||||
~GilSafeCallable()
|
||||
{
|
||||
if (fn) {
|
||||
PythonGILState gil;
|
||||
if (gil)
|
||||
fn = py::object();
|
||||
else
|
||||
(void) fn.release();
|
||||
}
|
||||
disable();
|
||||
}
|
||||
};
|
||||
using CallablePtr = std::shared_ptr<GilSafeCallable>;
|
||||
@@ -166,11 +172,34 @@ public:
|
||||
}
|
||||
return out;
|
||||
}
|
||||
void bind_callback(const CallablePtr& callback, const std::string& plugin_key)
|
||||
{
|
||||
if (!callback)
|
||||
return;
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
m_callbacks[plugin_key].push_back(callback);
|
||||
}
|
||||
std::vector<CallablePtr> take_callbacks_for_plugin(const std::string& plugin_key)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
auto it = m_callbacks.find(plugin_key);
|
||||
if (it == m_callbacks.end())
|
||||
return {};
|
||||
std::vector<CallablePtr> callbacks;
|
||||
callbacks.reserve(it->second.size());
|
||||
for (const std::weak_ptr<GilSafeCallable>& weak_callback : it->second) {
|
||||
if (auto callback = weak_callback.lock())
|
||||
callbacks.push_back(std::move(callback));
|
||||
}
|
||||
m_callbacks.erase(it);
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex m_mtx;
|
||||
std::unordered_map<int, wxWindow*> m_resources;
|
||||
std::unordered_map<int, std::string> m_owners;
|
||||
std::unordered_map<std::string, std::vector<std::weak_ptr<GilSafeCallable>>> m_callbacks;
|
||||
int m_next_id{1};
|
||||
};
|
||||
|
||||
@@ -448,6 +477,46 @@ void progress_close(int id)
|
||||
});
|
||||
}
|
||||
|
||||
void plater_notification(NotificationManager::NotificationLevel notification_level, const std::string& text,
|
||||
const std::string& hypertext, py::object on_click)
|
||||
{
|
||||
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
|
||||
CallablePtr holder = make_holder(std::move(on_click));
|
||||
if (holder)
|
||||
UiRegistry::instance().bind_callback(holder, plugin_key);
|
||||
|
||||
std::function<bool(wxEvtHandler*)> callback;
|
||||
if (holder) {
|
||||
callback = [holder](wxEvtHandler*) -> bool {
|
||||
if (!holder->active.load(std::memory_order_acquire))
|
||||
return false;
|
||||
|
||||
PythonGILState gil;
|
||||
if (!gil)
|
||||
return false;
|
||||
try {
|
||||
py::object result = holder->fn();
|
||||
return result.is_none() || result.cast<bool>();
|
||||
} catch (py::error_already_set& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised: " << e.what();
|
||||
PyErr_Clear();
|
||||
return false;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised: " << e.what();
|
||||
return false;
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised an unknown exception";
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
run_on_ui_blocking([notification_level, text, hypertext, callback = std::move(callback)]() mutable {
|
||||
wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::CustomNotification, notification_level, text,
|
||||
hypertext, std::move(callback));
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PluginHostUi::RegisterBindings(pybind11::module_& host)
|
||||
@@ -530,6 +599,23 @@ void PluginHostUi::RegisterBindings(pybind11::module_& host)
|
||||
ui.def("create_progress_dialog", &ui_create_progress_dialog, py::arg("title"), py::arg("message"),
|
||||
py::arg("maximum") = 100, py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE,
|
||||
"Create a native progress dialog and return a ProgressDialog handle.");
|
||||
|
||||
py::enum_<NotificationManager::NotificationLevel>(ui, "NotificationLevel")
|
||||
.value("ProgressBarNotificationLevel", NotificationManager::NotificationLevel::ProgressBarNotificationLevel)
|
||||
.value("HintNotificationLevel", NotificationManager::NotificationLevel::HintNotificationLevel)
|
||||
.value("RegularNotificationLevel", NotificationManager::NotificationLevel::RegularNotificationLevel)
|
||||
.value("PrintInfoNotificationLevel", NotificationManager::NotificationLevel::PrintInfoNotificationLevel)
|
||||
.value("PrintInfoShortNotificationLevel", NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel)
|
||||
.value("ImportantNotificationLevel", NotificationManager::NotificationLevel::ImportantNotificationLevel)
|
||||
.value("WarningNotificationLevel", NotificationManager::NotificationLevel::WarningNotificationLevel)
|
||||
.value("SeriousWarningNotificationLevel", NotificationManager::NotificationLevel::SeriousWarningNotificationLevel)
|
||||
.value("ErrorNotificationLevel", NotificationManager::NotificationLevel::ErrorNotificationLevel)
|
||||
.export_values();
|
||||
|
||||
ui.def("push_notification", &plater_notification, py::arg("notification_level"), py::arg("text"),
|
||||
py::arg("hyper_text") = "", py::arg("on_click") = py::none(),
|
||||
"Push a plater notification. hyper_text is an underlined label; on_click() is called when it is clicked "
|
||||
"and may return True to close the notification.");
|
||||
}
|
||||
|
||||
void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
|
||||
@@ -538,6 +624,9 @@ void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
|
||||
return;
|
||||
|
||||
auto teardown = [plugin_key]() {
|
||||
for (auto& callback : UiRegistry::instance().take_callbacks_for_plugin(plugin_key))
|
||||
callback->disable();
|
||||
|
||||
// Destroy() bypasses wxEVT_CLOSE, so the plugin's on_close is not fired on
|
||||
// forced teardown (intended); the resource destructor still cleans the registry.
|
||||
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {
|
||||
|
||||
@@ -7,9 +7,14 @@
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace Slic3r;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
@@ -418,3 +423,162 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
// 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 travels 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. Travels (no E) keep the junction vector purely geometric, as the formulas below assume.
|
||||
std::string corner_gcode(double turn_deg, double orientation_deg)
|
||||
{
|
||||
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 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 << " F9000\n"
|
||||
<< "G1 X" << junction_x + len * std::cos(a_out) << " Y" << junction_y + len * std::sin(a_out) << " 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 &&
|
||||
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)
|
||||
{
|
||||
GCodeProcessor proc;
|
||||
run_processor(proc, make_junction_config(flavor, corner_velocity, junction_deviation),
|
||||
corner_gcode(turn_deg, orientation_deg).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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user