mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-24 02:17:57 +00:00
Normalize the junction direction vector over XYZE (#15308)
* Normalize the junction direction vector over XYZE calc_vmax_junction_deviation() treats the dot product of two jd_unit_vec as a cosine, but the vectors were scaled by 1 / block.distance, which is the XYZ length. On an extruding move the E component then pushes the 4D norm above 1 and the dot product below -1, so the corner reads as straighter than it is and is planned too fast -- the more so the higher the flow. Measured on a 6 degree corner at scv 5: 86.9mm/s with no extrusion, 94.4mm/s at 0.029mm/mm, 150.0mm/s at 0.1mm/mm. Neither firmware does that. Marlin normalizes over XYZE for any extruding move (planner.cpp: `if (... || esteps > 0) normalize_junction_vector(unit_vec)`) and Klipper leaves E out of the cosine entirely, dotting only axes_r[0..2] (toolhead.py::Move.calc_junction). Normalizing satisfies both: with E normalized in, the cosine differs from the XYZ-only one by ~1e-5 at printing flow rates. This is a deliberate divergence from PrusaSlicer, which still scales by 1 / distance -- it carries an older Marlin's behaviour. Travel moves are unaffected, their vector was already unit length. Reported by Copilot in review of #15304. * Test that extrusion rate does not change corner planning The junction deviation tests were all travel-only, which is exactly why the E component of the junction vector went unchecked. Cover it: the same corner has to be planned the same whether nothing, an ordinary 0.42 x 0.2 line, or a fat large-nozzle line is extruded through it, on both Klipper and Marlin 2. Reported by Copilot in review of #15304.
This commit is contained in:
@@ -5037,10 +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);
|
||||
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;
|
||||
block.move_type = type;
|
||||
@@ -5415,10 +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);
|
||||
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;
|
||||
block.move_type = type;
|
||||
@@ -7245,6 +7245,11 @@ float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const
|
||||
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
|
||||
|
||||
@@ -637,9 +637,8 @@ 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.
|
||||
// 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();
|
||||
|
||||
@@ -468,22 +468,27 @@ FullPrintConfig make_junction_config(GCodeFlavor flavor, double corner_velocity,
|
||||
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`.
|
||||
// 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. Travels (no E) keep the junction vector purely geometric, as the formulas below assume.
|
||||
std::string corner_gcode(double turn_deg, double orientation_deg)
|
||||
// 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 << " F9000\n"
|
||||
<< "G1 X" << junction_x + len * std::cos(a_out) << " Y" << junction_y + len * std::sin(a_out) << " F9000\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();
|
||||
}
|
||||
|
||||
@@ -492,7 +497,7 @@ std::string corner_gcode(double turn_deg, double orientation_deg)
|
||||
double corner_speed(const GCodeProcessorResult& r)
|
||||
{
|
||||
for (const auto& mv : r.moves)
|
||||
if (mv.type == EMoveType::Travel &&
|
||||
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;
|
||||
@@ -500,11 +505,11 @@ double corner_speed(const GCodeProcessorResult& r)
|
||||
}
|
||||
|
||||
double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double junction_deviation,
|
||||
double turn_deg, double orientation_deg = 0.0)
|
||||
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).c_str());
|
||||
corner_gcode(turn_deg, orientation_deg, e_per_mm).c_str());
|
||||
return corner_speed(proc.get_result());
|
||||
}
|
||||
|
||||
@@ -582,3 +587,30 @@ TEST_CASE("Junction deviation is only used where the firmware actually plans wit
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user