mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 13:19:20 +02:00
A turnout's diverging track does not leave the through track tangentially. It runs straight along the through track to the blade tip (odcinek przediglicowy), breaks away there by the blade angle (kąt nagięcia iglicy, beta), runs straight over the blade to its heel, and only then curves - through alfa - beta, not the whole crossing angle - to the frog. Give TurnoutSegment a turn_in: a signed heading break applied before the segment is laid, which is the only way to express a track leaving by an angle rather than a tangent. lay_turnout then places the lead, the break and the blade ahead of the curve and hands the curve the angle that is actually left to spend; a zero blade length keeps the break and starts the curve at the tip. Reject a blade angle that is negative or already reaches the crossing angle. All-zero blade figures lay exactly the old tangent arc from PR, so existing layouts are untouched. Junction and turnout_preset carry the three new figures through to the panel; the presets themselves still hold zeros until the Id-1 catalogue numbers are in. Add test_turnout: the lead sits on the through track, the blade breaks by beta, and the frog still lands on the full crossing angle and the catalogue length, mirrored for a right-hand turnout.
131 lines
4.7 KiB
C++
131 lines
4.7 KiB
C++
#include <cmath>
|
|
#include <numbers>
|
|
|
|
#include "check.hpp"
|
|
#include "maj0sted/domain/geometry/turnout.hpp"
|
|
|
|
using namespace maj0sted::domain;
|
|
using maj0sted::domain::geometry::Pose;
|
|
|
|
namespace {
|
|
|
|
Turnout rz_1_9_r190() {
|
|
Turnout turnout{CrossingMark{9.0}, DivergeSide::Left, DivergingCurve{}, 27.138};
|
|
turnout.curve.arcs.push_back(TurnoutArc{190.0, 0.0, 0.0});
|
|
return turnout;
|
|
}
|
|
|
|
// Walks the laid path and returns the pose at the end of segment @p index.
|
|
Pose pose_after(const TurnoutGeometry& geometry, std::size_t index) {
|
|
Pose pose{0.0, 0.0, 1.0, 0.0};
|
|
for (std::size_t i = 0; i <= index; ++i) {
|
|
const auto& segment = geometry.path[i];
|
|
if (segment.turn_in != 0.0) {
|
|
const double c = std::cos(segment.turn_in);
|
|
const double s = std::sin(segment.turn_in);
|
|
pose = Pose{pose.x, pose.y, pose.hx * c - pose.hy * s,
|
|
pose.hx * s + pose.hy * c};
|
|
}
|
|
pose = geometry::layout_segment(segment.k0, segment.k1, segment.length, pose,
|
|
nullptr);
|
|
}
|
|
return pose;
|
|
}
|
|
|
|
void pre_blade_runs_along_the_through_track() {
|
|
auto turnout = rz_1_9_r190();
|
|
turnout.pre_blade = 3.5;
|
|
turnout.blade_angle = 0.02;
|
|
turnout.blade_length = 6.0;
|
|
|
|
const auto geometry = lay_turnout(Pose{0.0, 0.0, 1.0, 0.0}, turnout);
|
|
CHECK(geometry.valid);
|
|
|
|
// the lead is straight, on the through track, and the blade tip sits at its end
|
|
const Pose tip = pose_after(geometry, 0);
|
|
CHECK(std::abs(tip.x - 3.5) < 1e-9);
|
|
CHECK(std::abs(tip.y) < 1e-12);
|
|
CHECK(std::abs(tip.hy) < 1e-12);
|
|
|
|
// the blade breaks away by beta exactly, and stays straight over its length
|
|
const Pose heel = pose_after(geometry, 1);
|
|
CHECK(std::abs(std::atan2(heel.hy, heel.hx) - 0.02) < 1e-9);
|
|
CHECK(std::abs(std::hypot(heel.x - tip.x, heel.y - tip.y) - 6.0) < 1e-9);
|
|
}
|
|
|
|
void the_frog_still_lands_on_the_crossing_angle() {
|
|
const double alfa = std::atan(1.0 / 9.0);
|
|
|
|
auto tangential = rz_1_9_r190();
|
|
auto bladed = rz_1_9_r190();
|
|
bladed.pre_blade = 3.5;
|
|
bladed.blade_angle = 0.02;
|
|
bladed.blade_length = 6.0;
|
|
|
|
for (const auto& turnout : {tangential, bladed}) {
|
|
const auto geometry = lay_turnout(Pose{0.0, 0.0, 1.0, 0.0}, turnout);
|
|
CHECK(geometry.valid);
|
|
// the curve spends only alfa - beta, so the frog heading is alfa either way
|
|
CHECK(std::abs(std::atan2(geometry.frog.hy, geometry.frog.hx) - alfa) < 1e-9);
|
|
// and the catalogue length is still measured along the through track
|
|
CHECK(std::abs(geometry.frog.x - 27.138) < 1e-6);
|
|
}
|
|
}
|
|
|
|
void a_blade_reaching_the_crossing_angle_is_rejected() {
|
|
auto turnout = rz_1_9_r190();
|
|
turnout.blade_angle = std::atan(1.0 / 9.0); // nothing left for the curve to turn
|
|
CHECK(!lay_turnout(Pose{0.0, 0.0, 1.0, 0.0}, turnout).valid);
|
|
|
|
turnout.blade_angle = -0.01;
|
|
CHECK(!lay_turnout(Pose{0.0, 0.0, 1.0, 0.0}, turnout).valid);
|
|
}
|
|
|
|
void a_bladeless_curve_still_starts_tangent() {
|
|
const auto geometry = lay_turnout(Pose{0.0, 0.0, 1.0, 0.0}, rz_1_9_r190());
|
|
CHECK(geometry.valid);
|
|
for (const auto& segment : geometry.path) CHECK(segment.turn_in == 0.0);
|
|
}
|
|
|
|
// With no blade length the break still stands: the curve simply starts at the tip.
|
|
void a_zero_length_blade_keeps_its_break() {
|
|
auto turnout = rz_1_9_r190();
|
|
turnout.pre_blade = 3.5;
|
|
turnout.blade_angle = 0.02;
|
|
|
|
const auto geometry = lay_turnout(Pose{0.0, 0.0, 1.0, 0.0}, turnout);
|
|
CHECK(geometry.valid);
|
|
CHECK(geometry.path.size() >= 2);
|
|
CHECK(std::abs(geometry.path[1].turn_in - 0.02) < 1e-12);
|
|
CHECK(std::abs(std::atan2(geometry.frog.hy, geometry.frog.hx) -
|
|
std::atan(1.0 / 9.0)) < 1e-9);
|
|
}
|
|
|
|
// A right-hand turnout is the mirror image: the blade breaks the other way.
|
|
void a_right_hand_blade_breaks_the_other_way() {
|
|
auto turnout = rz_1_9_r190();
|
|
turnout.side = DivergeSide::Right;
|
|
turnout.pre_blade = 3.5;
|
|
turnout.blade_angle = 0.02;
|
|
turnout.blade_length = 6.0;
|
|
|
|
const auto geometry = lay_turnout(Pose{0.0, 0.0, 1.0, 0.0}, turnout);
|
|
CHECK(geometry.valid);
|
|
CHECK(std::abs(std::atan2(pose_after(geometry, 1).hy, pose_after(geometry, 1).hx) +
|
|
0.02) < 1e-9);
|
|
CHECK(std::abs(std::atan2(geometry.frog.hy, geometry.frog.hx) +
|
|
std::atan(1.0 / 9.0)) < 1e-9);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
RUN(pre_blade_runs_along_the_through_track);
|
|
RUN(the_frog_still_lands_on_the_crossing_angle);
|
|
RUN(a_blade_reaching_the_crossing_angle_is_rejected);
|
|
RUN(a_bladeless_curve_still_starts_tangent);
|
|
RUN(a_zero_length_blade_keeps_its_break);
|
|
RUN(a_right_hand_blade_breaks_the_other_way);
|
|
return REPORT();
|
|
}
|