17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 14:29:20 +02:00
Files
maszyna/editor/tests/test_turnout.cpp
maj00r 321c9dd7ab Editor: fold the plan library into the tree as modules, drop its own name
The track layout library came in as a subproject of its own -- its own
CMakeLists, its own install and export rules, its own include tree -- which only
made the build harder to follow now that everything else is a module. It is part
of the editor, so it lives where the editor lives.

- editor/plan_*.cppm / plan_*.cpp: the fifteen interface units and twelve
  implementation units, flat in editor/ beside editorTerrain and orthophoto, with
  the plan_ prefix keeping them clear of what was already there (editor/turnouts
  is the preset catalogue, so the geometry is plan_turnout_geometry).
- modules are named eu07.editor.plan_*, like every other module in the tree.
- namespaces move under the editor's own: editor::plan, editor::plan::geometry,
  ::io, ::render. The library's old name is gone from the sources and the docs.
- every moved file carries the MPL header the rest of the tree carries; it had
  none. version.hpp went with the subproject: macros only, and unused.
- the umbrella header is dropped -- nothing imported it.
- the unit tests survive the fold: editor/tests, behind EU07_EDITOR_PLAN_TESTS
  (off by default), compiles the interfaces a second time into a small library of
  its own, because a test can only import modules from a target it links and
  linking the game would drag the whole simulator in. The code under test needs
  nothing but a compiler. 7/7 pass.
- a switch may now be written with two dashes as well: --editor beside -editor.

Builds with clang 22 (-DWITH_BETTER_RENDERER=OFF); -editor and --editor both come
up in the editor on empty ground.
2026-09-13 13:31:01 +02:00

458 lines
21 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include <cmath>
#include <numbers>
#include "check.hpp"
#include <initializer_list>
#include <vector>
import eu07.editor.plan_turnout_geometry;
// A turnout is laid from the list its catalogue entry gives, piece by piece, and
// nothing in it is fitted: whether the list keeps the promises of the crossing
// mark and the catalogue length is reported, not corrected. The blade is
// dimensioned on top of that, the way Koc's rys. 5.9 dimensions an iglica styczna
// do opornicy o ściętym ostrzu: the curve is theoretically tangent to the stock
// rail at PR, but the blade only starts at a, and its planed nose runs on to A
// where the rail is u thick.
using namespace editor::plan;
using editor::plan::geometry::Pose;
namespace {
constexpr double kAlfa19 = 0.11065722117389563; // atan(1/9)
constexpr double kStart[]{0.0, 0.0, 1.0, 0.0};
// Rz 49E1-190-1:9 as the drawings have it: u and d from Koc's tablica 5.1, the
// rear tangent from rys. 5.13 (27138 = 10523 + 16615, krzyżownica prosta). The
// blade ends where the gap to the stock rail takes the przekładka in its heel.
constexpr double kRadius = 190.0;
constexpr double kTipThickness = 0.0049; // u
constexpr double kNose = 0.1250; // d
constexpr double kRailtop = 0.070; // w
constexpr double kHeelOffset = 0.136;
constexpr double kBackTangent = 16.615; // b
// a: the odcinek przediglicowy, derived from u and d rather than copied — the gap
// opens on the blade's own curve, and the station it opens at is on the axis
const double kLead = blade_run_to_offset(kRadius, 0.0, kTipThickness) - kNose;
const double kBlade = blade_run_to_offset(kRadius, 0.0, kHeelOffset) - kLead;
// The gap @p pose has opened between the blade and the stock rail: the axis offset
// plus what half a gauge of rail swings out of the way as the path turns off.
double gap_at(const Pose& start, const Pose& pose) {
const double offset = (pose.x - start.x) * -start.hy + (pose.y - start.y) * start.hx;
const double heading = std::atan2(pose.hy * start.hx - pose.hx * start.hy,
pose.hx * start.hx + pose.hy * start.hy);
return offset + kHalfGauge * (1.0 - std::cos(heading));
}
const double kFrontTangent = kRadius * std::tan(0.5 * kAlfa19);
const double kFrogRail = kBackTangent - kFrontTangent;
Turnout rz_1_9_r190() {
Turnout turnout{CrossingMark{9.0},
DivergeSide::Left,
{},
kFrontTangent + kBackTangent,
Blade{kTipThickness, kNose, kRailtop}};
turnout.path.push_back(TurnoutPiece{TurnoutPart::PreBlade, kLead, kRadius, kRadius, 0.0});
turnout.path.push_back(TurnoutPiece{TurnoutPart::Blade, kBlade, kRadius, kRadius, 0.0});
turnout.path.push_back(TurnoutPiece{TurnoutPart::Curve,
kRadius * kAlfa19 - kLead - kBlade, kRadius,
kRadius, 0.0});
turnout.path.push_back(TurnoutPiece{TurnoutPart::FrogRail, kFrogRail, 0.0, 0.0, 0.0});
return turnout;
}
TurnoutGeometry laid(const Turnout& turnout) {
return lay_turnout(Pose{kStart[0], kStart[1], kStart[2], kStart[3]}, 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 = editor::plan::geometry::layout_segment(segment.k0, segment.k1, segment.length, pose,
nullptr);
}
return pose;
}
void a_tangential_blade_never_breaks_away() {
const auto geometry = laid(rz_1_9_r190());
CHECK(geometry.valid);
for (const auto& segment : geometry.path) {
CHECK(segment.turn_in == 0.0);
}
// the curve is there from PR: the odcinek przediglicowy is already on it, the
// vehicle is simply still on the through track
CHECK(std::abs(geometry.path[0].k0 - 1.0 / kRadius) < 1e-12);
CHECK(std::abs(geometry.path[1].k0 - 1.0 / kRadius) < 1e-12);
// the blade begins where its piece does, a from PR
CHECK(geometry.has_blade);
CHECK(std::abs(geometry.blade_station - kLead) < 1e-12);
CHECK(std::abs(geometry.blade_tip.x - kRadius * std::sin(kLead / kRadius)) < 1e-9);
CHECK(std::abs(geometry.blade_tip.y -
kRadius * (1.0 - std::cos(kLead / kRadius))) < 1e-9);
}
void a_planed_nose_reaches_its_thickness_where_the_table_says() {
const auto geometry = laid(rz_1_9_r190());
CHECK(geometry.valid);
const Pose start{kStart[0], kStart[1], kStart[2], kStart[3]};
// A: where the blade stands u away from the stock rail, and where the tangent
// there is the kąt przylegania — a couple of dozen minutes
CHECK(std::abs(gap_at(start, geometry.blade_nose_end) - kTipThickness) < 1e-9);
CHECK(std::abs(geometry.blade_angle - geometry.nose_end / kRadius) < 1e-9);
CHECK(geometry.blade_angle * 180.0 / std::numbers::pi > 0.40);
CHECK(geometry.blade_angle * 180.0 / std::numbers::pi < 0.42);
// a was cut so that A lands on a + d, so a type carrying all three agrees with
// the curve it is laid on and the residual is nothing
CHECK(std::abs(geometry.nose_end - (kLead + kNose)) < 1e-12);
CHECK(std::abs(geometry.blade_residual) < 1e-12);
// the planing runs from the ostrze to full railhead width
CHECK(std::abs(geometry.planing_end - blade_run_to_offset(kRadius, 0.0, kRailtop)) <
1e-12);
CHECK(std::abs(gap_at(start, geometry.blade_planed) - kRailtop) < 1e-9);
CHECK(std::abs(geometry.planing_length - (geometry.planing_end - kLead)) < 1e-12);
CHECK(geometry.planing_end > geometry.nose_end);
}
void a_lead_derived_from_u_and_d_lands_on_the_table() {
// Koc, tablica 5.1: a, d and u for the iglice styczne do opornicy o ściętym
// ostrzu. They obey a + d = the run to a gap of u, which is what lets a preset
// carry only u and d. The table dimensions it on the rail, as a drawing does, so
// the axis station is scaled back up by R1/R; the numbers then agree to a hair at
// 1200 m and to millimetres at 190
struct Row {
double radius, a, d, u, tolerance;
};
constexpr Row rows[]{
{190.0, 1.2483, 0.1250, 0.0049, 7e-3}, {300.0, 1.5000, 0.1250, 0.0044, 2e-3},
{500.0, 2.1650, 0.1250, 0.0053, 1.4e-2}, {760.0, 2.6330, 0.1250, 0.0050, 2e-4},
{1200.0, 3.3347, 0.1304, 0.0050, 1e-4},
};
for (const auto& row : rows) {
const double rail = row.radius + kHalfGauge;
const double drawn =
blade_run_to_offset(row.radius, 0.0, row.u) * rail / row.radius;
CHECK(std::abs(drawn - (row.a + row.d)) < row.tolerance);
}
}
void a_straight_blade_is_planed_by_its_break() {
// iglica prosta sieczna: the same two dimensions, taken off the break angle
const double beta = 0.02;
Turnout turnout{CrossingMark{9.0}, DivergeSide::Left, {}, 0.0,
Blade{0.005, 0.0, 0.070}};
turnout.path.push_back(TurnoutPiece{TurnoutPart::PreBlade, 3.5, 0.0, 0.0, 0.0});
turnout.path.push_back(TurnoutPiece{TurnoutPart::Blade, 6.0, 0.0, 0.0, beta});
turnout.path.push_back(TurnoutPiece{TurnoutPart::Curve, 190.0 * (kAlfa19 - beta),
190.0, 190.0, 0.0});
const auto geometry = laid(turnout);
CHECK(geometry.valid);
// the lead runs along the through track and the blade tip sits at its end
CHECK(geometry.blade_station == 3.5);
CHECK(std::abs(geometry.blade_tip.x - 3.5) < 1e-9);
CHECK(geometry.blade_tip.y == 0.0);
CHECK(std::abs(geometry.nose_end - (3.5 + 0.005 / std::sin(beta))) < 1e-12);
CHECK(std::abs(geometry.planing_length - 0.070 / std::sin(beta)) < 1e-12);
// Templot says the same length along the stock rail rather than along the blade
CHECK(std::abs(geometry.planing_length * std::cos(beta) - 0.070 / std::tan(beta)) <
1e-12);
// the break itself is the kąt przylegania here: the blade is straight
CHECK(std::abs(geometry.blade_angle - beta) < 1e-12);
}
void a_blade_without_a_cut_tip_is_dimensioned_at_its_own_start() {
auto turnout = rz_1_9_r190();
turnout.blade = Blade{0.0, 0.0, 0.0};
const auto geometry = laid(turnout);
CHECK(geometry.valid);
CHECK(geometry.has_blade);
CHECK(std::abs(geometry.blade_station - kLead) < 1e-12);
CHECK(geometry.nose_end == 0.0);
CHECK(geometry.planing_length == 0.0);
CHECK(geometry.blade_residual == 0.0);
}
void a_catalogue_list_closes_on_its_own_numbers() {
const auto geometry = laid(rz_1_9_r190());
CHECK(geometry.valid);
CHECK(std::abs(geometry.angle_residual) < 1e-9);
CHECK(std::abs(std::atan2(geometry.frog.hy, geometry.frog.hx) - kAlfa19) < 1e-9);
// the two tangent legs are the drawing's a and b, and L is their sum
CHECK(std::abs(geometry.tangent_front - 10.523) < 1e-3);
CHECK(std::abs(geometry.tangent_back - 16.615) < 1e-3);
CHECK(std::abs(geometry.tangent_front + geometry.tangent_back - 27.138) < 1e-3);
CHECK(std::abs(geometry.length_residual) < 1e-9);
}
void the_switch_ends_on_the_straight_too() {
const auto geometry = laid(rz_1_9_r190());
CHECK(geometry.valid);
// koniec rozjazdu as the straight sees it: the frog projected back onto it, which
// is shorter than a + b and is the second point marked on the tor zasadniczy
CHECK(std::abs(geometry.through_length -
(kRadius * std::sin(kAlfa19) + kFrogRail * std::cos(kAlfa19))) < 1e-9);
CHECK(geometry.through_length < geometry.tangent_front + geometry.tangent_back);
CHECK(std::abs(geometry.through_end.x - geometry.through_length) < 1e-12);
CHECK(geometry.through_end.y == 0.0);
CHECK(geometry.through_end.hx == 1.0);
}
void a_list_that_misses_the_mark_is_still_laid_and_said_out_loud() {
auto turnout = rz_1_9_r190();
turnout.path[2].length -= 2.0; // two metres of curve short of the crossing angle
const auto geometry = laid(turnout);
CHECK(geometry.valid); // it lays: the numbers are the author's business
CHECK(std::abs(geometry.angle_residual + 2.0 / kRadius) < 1e-9);
CHECK(geometry.length_residual != 0.0);
}
void a_degenerate_piece_is_refused() {
auto turnout = rz_1_9_r190();
turnout.path[2].length = 0.0;
CHECK(!laid(turnout).valid);
turnout = rz_1_9_r190();
turnout.path[1].radius_start = -190.0;
CHECK(!laid(turnout).valid);
turnout = rz_1_9_r190();
turnout.path.clear();
CHECK(!laid(turnout).valid);
}
void a_right_hand_turnout_is_the_mirror_of_a_left_one() {
const auto left = laid(rz_1_9_r190());
auto turnout = rz_1_9_r190();
turnout.side = DivergeSide::Right;
const auto geometry = laid(turnout);
CHECK(geometry.valid);
CHECK(std::abs(geometry.path[1].k0 + 1.0 / kRadius) < 1e-12);
CHECK(std::abs(std::atan2(geometry.frog.hy, geometry.frog.hx) + kAlfa19) < 1e-9);
CHECK(std::abs(geometry.through_length - left.through_length) < 1e-9);
// the residual is not signed by the hand: a right turnout closes just as well
CHECK(std::abs(geometry.angle_residual) < 1e-9);
// and the blade is dimensioned the same, on the other side
CHECK(std::abs(geometry.nose_end - left.nose_end) < 1e-12);
CHECK(std::abs(geometry.blade_angle - left.blade_angle) < 1e-12);
CHECK(std::abs(geometry.blade_nose_end.y + left.blade_nose_end.y) < 1e-12);
}
void a_basket_is_three_pieces_and_nothing_special() {
// łuk koszowy: 190 m eased to 300 m by a clothoid, ending on the same angle
const double first = 8.0;
const double transition = 6.0;
// deflection: first/190 + transition*(1/190 + 1/300)/2 + rest/300 == alfa
const double turned = first / 190.0 + 0.5 * transition * (1.0 / 190.0 + 1.0 / 300.0);
const double rest = (kAlfa19 - turned) * 300.0;
Turnout turnout{CrossingMark{9.0}, DivergeSide::Left, {}, 0.0,
Blade{0.0049, 0.125, 0.070}};
turnout.path.push_back(TurnoutPiece{TurnoutPart::Blade, first, 190.0, 190.0, 0.0});
turnout.path.push_back(TurnoutPiece{TurnoutPart::Curve, transition, 190.0, 300.0, 0.0});
turnout.path.push_back(TurnoutPiece{TurnoutPart::Curve, rest, 300.0, 300.0, 0.0});
const auto geometry = laid(turnout);
CHECK(geometry.valid);
CHECK(std::abs(geometry.angle_residual) < 1e-9);
CHECK(std::abs(std::atan2(geometry.frog.hy, geometry.frog.hx) - kAlfa19) < 1e-9);
const Pose end = pose_after(geometry, geometry.path.size() - 1);
CHECK(std::abs(end.x - geometry.frog.x) < 1e-9);
CHECK(std::abs(end.y - geometry.frog.y) < 1e-9);
// a blade starting at PR has no lead ahead of it, so a is nothing and the
// residual is what the type's own d asks for against the curve
CHECK(geometry.blade_station == 0.0);
CHECK(std::abs(geometry.blade_residual - (0.125 - geometry.nose_end)) < 1e-12);
}
TurnoutGeometry laid_bent(const Turnout& turnout, double bend) {
return lay_turnout(Pose{kStart[0], kStart[1], kStart[2], kStart[3]}, turnout, bend);
}
// The bend that turns the tor zasadniczy by @p beta over the turnout. Every leg of
// the triangle has the same tangent t, so a radius is t/tg(half its own angle).
double bend_for(double beta) { return std::tan(0.5 * beta) / kFrontTangent; }
void a_bend_of_nothing_is_the_turnout_it_was() {
const auto plain = laid(rz_1_9_r190());
const auto bent = laid_bent(rz_1_9_r190(), 0.0);
CHECK(bent.valid);
CHECK(bent.bend == 0.0);
CHECK(bent.diverging_curvature == 0.0);
CHECK(bent.path.size() == plain.path.size());
for (std::size_t i = 0; i < plain.path.size(); ++i) {
CHECK(bent.path[i].k0 == plain.path[i].k0);
CHECK(bent.path[i].length == plain.path[i].length);
}
CHECK(bent.through_length == plain.through_length);
CHECK(bent.nose_end == plain.nose_end);
}
void a_symmetric_bend_of_the_1_9_r190_is_the_catalogue_rls() {
// Koc, 5.3.6: Rłs 49E1-380,292-1:9 z krzyżownicą prostą powstaje przez wygięcie
// Rz 49E1-190-1:9. Symmetric means the two tracks split the crossing angle, so
// the tor zasadniczy turns half of it the other way and both radii come out the
// same — and the catalogue's own 380,292 is what they come out at
const auto geometry = laid_bent(rz_1_9_r190(), bend_for(-0.5 * kAlfa19));
CHECK(geometry.valid);
CHECK(geometry.bend < 0.0);
CHECK(geometry.diverging_curvature > 0.0);
CHECK(std::abs(std::abs(1.0 / geometry.bend) - 380.292) < 0.02);
CHECK(std::abs(std::abs(1.0 / geometry.diverging_curvature) - 380.292) < 0.02);
CHECK(std::abs(geometry.bend_angle + 0.5 * kAlfa19) < 1e-12);
}
void a_bend_lands_on_kocs_own_formulas() {
// (5.7) jednostronny and (5.8) dwustronny. The second is written here with the
// sign our curvature carries openly: Koc's R - R1 denominator is the same figure
// read off a drawing that has already decided which way the branch goes
const double t2 = kFrontTangent * kFrontTangent;
for (const double r1 : {320.0, 600.0, 2000.0}) {
const auto same = laid_bent(rz_1_9_r190(), 1.0 / r1);
CHECK(same.valid);
CHECK(std::abs(1.0 / same.diverging_curvature -
(kRadius * r1 - t2) / (kRadius + r1)) < 1e-9);
// bending with the branch makes it sharper, which is why R190 may not be
CHECK(1.0 / same.diverging_curvature < kRadius);
const auto against = laid_bent(rz_1_9_r190(), -1.0 / r1);
CHECK(against.valid);
CHECK(std::abs(1.0 / against.diverging_curvature -
(kRadius * r1 + t2) / (r1 - kRadius)) < 1e-9);
CHECK(1.0 / against.diverging_curvature > kRadius);
}
}
void a_bend_that_straightens_the_branch_still_lays() {
// wygięcie dwustronne aż tor zwrotny wychodzi prosty: the triangle is all
// tangent and no arc, so the path from PR is 2t of straight and the frog rail
const auto geometry = laid_bent(rz_1_9_r190(), bend_for(-kAlfa19));
CHECK(geometry.valid);
CHECK(geometry.diverging_curvature == 0.0);
CHECK(std::abs(geometry.diverging_length -
(2.0 * kFrontTangent + kFrogRail)) < 1e-9);
CHECK(std::abs(geometry.frog.hy) < 1e-12); // it runs dead straight
}
void bending_leaves_the_triangle_alone() {
// what łukowanie does not touch: the crossing angle, the tangent legs, and so
// the catalogue length. that is the whole point of rotating the triangle
for (const double beta : {-0.5 * kAlfa19, -0.02, 0.01, 0.03}) {
const auto geometry = laid_bent(rz_1_9_r190(), bend_for(beta));
CHECK(geometry.valid);
CHECK(std::abs(geometry.angle_residual) < 1e-9);
CHECK(std::abs(geometry.tangent_front - kFrontTangent) < 1e-9);
CHECK(std::abs(geometry.tangent_back - kBackTangent) < 1e-9);
CHECK(std::abs(geometry.length_residual) < 1e-9);
CHECK(std::abs(geometry.bend_angle - beta) < 1e-12);
}
}
void a_bent_switch_keeps_its_blade() {
// Koc: zwrotnica i krzyżownica pozostają bez zmian co do wymiarów, należy je
// tylko wygiąć. The blade opens against an opornica that curves with it, so what
// dimensions it is the difference of the two — and a + d still lands on A, to
// well under the millimetre the rail is cut to
const auto geometry = laid_bent(rz_1_9_r190(), bend_for(-0.5 * kAlfa19));
CHECK(geometry.valid);
CHECK(geometry.has_blade);
// the rails are the same rails: a and the blade keep their lengths exactly
CHECK(geometry.blade_station == kLead);
CHECK(std::abs(geometry.path[1].length - kBlade) < 1e-12);
CHECK(std::abs(geometry.nose_end - (kLead + kNose)) < 1e-3);
CHECK(std::abs(geometry.blade_residual) < 1e-3);
CHECK(geometry.blade_angle > 0.0);
// and the tip sits on the bent path: still off to the left, but half as far as
// it was, because the tor zasadniczy now runs away under it
const auto plain = laid(rz_1_9_r190());
CHECK(geometry.blade_tip.y > 0.0);
CHECK(geometry.blade_tip.y < plain.blade_tip.y);
}
void the_bent_switch_ends_on_its_own_arc() {
const double bend = bend_for(-0.5 * kAlfa19);
const auto geometry = laid_bent(rz_1_9_r190(), bend);
CHECK(geometry.valid);
// koniec rozjazdu is the frog brought back onto the tor zasadniczy along the
// radius, so it lands on the arc rather than on the straight PR set out along
const double radius = 1.0 / bend;
const double centre_y = radius;
CHECK(std::abs(std::hypot(geometry.through_end.x,
geometry.through_end.y - centre_y) - std::abs(radius)) < 1e-9);
CHECK(geometry.through_length > 0.0);
CHECK(geometry.through_length < geometry.tangent_front + geometry.tangent_back);
CHECK(std::abs(geometry.through_end.y) > 1e-6); // the through track has left y=0
}
void a_path_with_no_one_radius_will_not_bend() {
// a łuk koszowy has no single triangle to rotate about, and a bend guessed at
// one of its radii would be a drawing of nothing
const double first = 8.0;
const double transition = 6.0;
const double turned = first / 190.0 + 0.5 * transition * (1.0 / 190.0 + 1.0 / 300.0);
Turnout turnout{CrossingMark{9.0}, DivergeSide::Left, {}, 0.0, Blade{0.0049, 0.125, 0.070}};
turnout.path.push_back(TurnoutPiece{TurnoutPart::Blade, first, 190.0, 190.0, 0.0});
turnout.path.push_back(TurnoutPiece{TurnoutPart::Curve, transition, 190.0, 300.0, 0.0});
turnout.path.push_back(
TurnoutPiece{TurnoutPart::Curve, (kAlfa19 - turned) * 300.0, 300.0, 300.0, 0.0});
CHECK(laid(turnout).valid);
CHECK(!laid_bent(turnout, 1.0 / 600.0).valid);
}
void the_tangent_lengths_meet_where_the_tracks_would() {
const auto geometry = laid(rz_1_9_r190());
CHECK(geometry.valid);
// the through tangent and the frog tangent meet at one point: walking either
// way from the two ends has to land on it
const double cx = geometry.tangent_front;
CHECK(std::abs(geometry.frog.x - geometry.frog.hx * geometry.tangent_back - cx) < 1e-9);
CHECK(std::abs(geometry.frog.y - geometry.frog.hy * geometry.tangent_back) < 1e-9);
}
} // namespace
int main() {
RUN(a_tangential_blade_never_breaks_away);
RUN(a_planed_nose_reaches_its_thickness_where_the_table_says);
RUN(a_lead_derived_from_u_and_d_lands_on_the_table);
RUN(a_straight_blade_is_planed_by_its_break);
RUN(a_blade_without_a_cut_tip_is_dimensioned_at_its_own_start);
RUN(a_catalogue_list_closes_on_its_own_numbers);
RUN(the_switch_ends_on_the_straight_too);
RUN(a_list_that_misses_the_mark_is_still_laid_and_said_out_loud);
RUN(a_degenerate_piece_is_refused);
RUN(a_right_hand_turnout_is_the_mirror_of_a_left_one);
RUN(a_basket_is_three_pieces_and_nothing_special);
RUN(the_tangent_lengths_meet_where_the_tracks_would);
RUN(a_bend_of_nothing_is_the_turnout_it_was);
RUN(a_symmetric_bend_of_the_1_9_r190_is_the_catalogue_rls);
RUN(a_bend_lands_on_kocs_own_formulas);
RUN(a_bend_that_straightens_the_branch_still_lays);
RUN(bending_leaves_the_triangle_alone);
RUN(a_bent_switch_keeps_its_blade);
RUN(the_bent_switch_ends_on_its_own_arc);
RUN(a_path_with_no_one_radius_will_not_bend);
return REPORT();
}