17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-08 18:50:34 +01:00

Merge remote-tracking branch 'origin/feat/switch-blades' into feat/renderers-cleanup

This commit is contained in:
2026-10-29 23:55:04 +01:00
145 changed files with 36946 additions and 3791 deletions

View File

@@ -14,6 +14,14 @@ add_executable(${UNIT_TESTS_NAME}
main.cpp
vehicle.cpp
engine_stubs.cpp
editor_stubs.cpp
# the geometry of the lines the track editor designs, standing on its own
"${CMAKE_SOURCE_DIR}/editor/editorAlignment.cpp"
# and the heights of the tracks laid
"${CMAKE_SOURCE_DIR}/editor/editorGroundFollow.cpp"
"${CMAKE_SOURCE_DIR}/editor/editorProfile.cpp"
# and the switches laid into the curves
"${CMAKE_SOURCE_DIR}/editor/editorSwitchCurve.cpp"
${UNIT_TEST_CASES})
# the core library brings the game's build settings along

View File

@@ -0,0 +1,13 @@
// stand-ins for the parts of the engine which the code of the editor under test reaches for: the translations give the English
// messages back as they are
#include "stdafx.h"
#include "utilities/translation.h"
locale Translations;
const char *locale::lookup_c(const char *msg, bool) { return msg; }
const std::string &locale::lookup_s(const std::string &msg, bool) { return msg; }
const char *locale::lookup_n(const char *singular, const char *plural, long long n) { return n == 1 ? singular : plural; }

File diff suppressed because it is too large Load Diff

66
tests/unit/test_area.cpp Normal file
View File

@@ -0,0 +1,66 @@
#include "stdafx.h"
#include "doctest.h"
#include "editor/editorGeometry.hpp"
#include <algorithm>
#include <cstdlib>
#include <set>
#include <utility>
#include <vector>
namespace {
std::set<std::pair<int, int>> squares(std::vector<glm::dvec2> const &Polygon, double const Size) {
auto const found{geometry::squares_in(Polygon, Size)};
return {found.begin(), found.end()};
}
} // namespace
TEST_CASE("download area: the tiles a triangle touches") {
// x + y < 1000: the squares of 256 m whose corner nearest the origin lies inside
auto const found{squares({{0.0, 0.0}, {1000.0, 0.0}, {0.0, 1000.0}}, 256.0)};
std::set<std::pair<int, int>> expected;
for (int x = 0; x < 4; ++x)
for (int y = 0; x + y < 4; ++y)
expected.emplace(x, y);
CHECK(found == expected);
}
TEST_CASE("download area: the tiles of a concave polygon leave out the ones in its notch") {
// an L, 300 m wide, its arms 1000 m long
auto const found{squares({{0.0, 0.0}, {1000.0, 0.0}, {1000.0, 300.0}, {300.0, 300.0}, {300.0, 1000.0}, {0.0, 1000.0}}, 256.0)};
std::set<std::pair<int, int>> expected;
for (int x = 0; x < 4; ++x)
expected.insert({{x, 0}, {x, 1}});
for (int y = 2; y < 4; ++y)
expected.insert({{0, y}, {1, y}});
CHECK(found == expected);
CHECK(found.count({2, 2}) == 0);
}
TEST_CASE("download area: a thin band along a line touches the tiles it crosses, none further") {
// 4 m wide along the diagonal, from (10, 10) to (2000, 2000)
auto const found{squares({{10.0, 8.0}, {2000.0, 1998.0}, {2000.0, 2002.0}, {10.0, 12.0}}, 256.0)};
for (int i = 0; i < 8; ++i)
CHECK(found.count({i, i}) == 1);
CHECK(std::none_of(found.begin(), found.end(), [](std::pair<int, int> const &Key) { return std::abs(Key.first - Key.second) > 1; }));
}
TEST_CASE("download area: a polygon inside one tile, or around a corner of four") {
CHECK(squares({{10.0, 10.0}, {20.0, 10.0}, {20.0, 20.0}, {10.0, 20.0}}, 256.0) == std::set<std::pair<int, int>>{{0, 0}});
CHECK(squares({{250.0, 250.0}, {262.0, 250.0}, {262.0, 262.0}, {250.0, 262.0}}, 256.0) == std::set<std::pair<int, int>>{{0, 0}, {1, 0}, {0, 1}, {1, 1}});
// in the grid of the coordinates of Poland, about 500 km from its origin
auto const distant{squares({{500100.0, 600100.0}, {500200.0, 600100.0}, {500200.0, 600200.0}}, 256.0)};
CHECK(distant == std::set<std::pair<int, int>>{{1953, 2344}});
}
TEST_CASE("download area: a point inside or outside, a square of no size") {
std::vector<glm::dvec2> const triangle{{0.0, 0.0}, {100.0, 0.0}, {0.0, 100.0}};
CHECK(geometry::square_touches(triangle, 10.0, 10.0, 0.0));
CHECK_FALSE(geometry::square_touches(triangle, 80.0, 80.0, 0.0));
// a square holding the whole polygon touches it, with no corner of its inside
CHECK(geometry::square_touches(triangle, -50.0, -50.0, 300.0));
// fewer than three points: no area
CHECK(geometry::squares_in({{0.0, 0.0}, {100.0, 100.0}}, 256.0).empty());
}

View File

@@ -0,0 +1,42 @@
#include "stdafx.h"
#include "doctest.h"
#include "editor/editorGroundFollow.hpp"
#include <cmath>
#include <limits>
namespace {
segment_data straight(double const From, double const To) {
segment_data piece;
piece.points[segment_data::point::start] = {From, 0.0, 0.0};
piece.points[segment_data::point::end] = {To, 0.0, 0.0};
return piece;
}
} // namespace
TEST_CASE("heights: from point to point the tracks meet at one height where a point is clicked just past a joint") {
// three tracks of 40 m, the grade breaking 3 m past the first joint and 2 m short of the second one, steeply
std::vector<segment_data> pieces{straight(0.0, 40.0), straight(40.0, 80.0), straight(80.0, 120.0)};
auto const unknown{std::numeric_limits<double>::quiet_NaN()};
ground_follow::follow_points(pieces, {{43.0, -5.0, 0.0}, {78.0, 3.0, 0.0}}, {0.0, unknown, 0.0, unknown});
REQUIRE(pieces.size() >= 3);
for (std::size_t i = 0; i + 1 < pieces.size(); ++i)
CHECK(pieces[i].points[segment_data::point::end].y == doctest::Approx(pieces[i + 1].points[segment_data::point::start].y).epsilon(1e-9));
CHECK(pieces.front().points[segment_data::point::start].y == doctest::Approx(0.0));
CHECK(pieces.back().points[segment_data::point::end].y == doctest::Approx(0.0));
}
TEST_CASE("heights: with no point between the ends the grade is even from one end to the other") {
std::vector<segment_data> pieces{straight(0.0, 50.0), straight(50.0, 100.0)};
auto const unknown{std::numeric_limits<double>::quiet_NaN()};
ground_follow::follow_points(pieces, {}, {10.0, unknown, 12.0, unknown});
REQUIRE(pieces.size() == 2);
CHECK(pieces[0].points[segment_data::point::end].y == doctest::Approx(11.0));
CHECK(pieces[1].points[segment_data::point::start].y == doctest::Approx(11.0));
}

View File

@@ -0,0 +1,245 @@
#include "stdafx.h"
#include "doctest.h"
#include "editor/editorSwitchCurve.hpp"
#include <algorithm>
#include <cmath>
#include <string>
namespace {
// a line every half metre: a straight, a transition curve from it to the arc of the radius, with a straight cant ramp along it, and
// the arc, each of the lengths given
std::vector<switch_curve::track_point> line(double const Straight, double const Transition, double const Arc, double const Radius, double const Cant) {
std::vector<switch_curve::track_point> points;
double heading{0.0};
auto const curvature = [&](double const S) { return S <= Straight ? 0.0 : S <= Straight + Transition ? (S - Straight) / Transition / Radius : 1.0 / Radius; };
auto const cant = [&](double const S) { return S <= Straight ? 0.0 : S <= Straight + Transition ? Cant * (S - Straight) / Transition : Cant; };
for (double s{0.0}; s <= Straight + Transition + Arc + 1e-9; s += 0.5) {
if (s > 0.0)
heading += 0.5 * (curvature(s - 0.5) + curvature(s)) * 0.5;
points.push_back({s, heading, cant(s)});
}
return points;
}
// the tracks of a switch Length long laid into the line from the station: the main one along it, the diverging one turning off it
// to the side by the curvature of the basic switch
std::pair<std::vector<switch_curve::track_point>, std::vector<switch_curve::track_point>> laid(std::vector<switch_curve::track_point> const &Line, double const From, double const Length, double const Radius, int const Side) {
std::vector<switch_curve::track_point> main;
std::vector<switch_curve::track_point> branch;
auto const first{std::lower_bound(Line.begin(), Line.end(), From, [](auto const &Point, double const Value) { return Point.station < Value; })};
for (auto point{first}; point != Line.end() && point->station <= From + Length + 1e-9; ++point) {
auto const s{point->station - From};
main.push_back({s, point->heading - first->heading, 0.0});
branch.push_back({s, point->heading - first->heading + Side * s / Radius, 0.0});
}
return {main, branch};
}
bool says(std::vector<std::string> const &Lines, char const *Text) {
return std::any_of(Lines.begin(), Lines.end(), [&](std::string const &Line) { return Line.find(Text) != std::string::npos; });
}
} // namespace
TEST_CASE("switch on a curve: the curvatures of its tracks on a transition curve, read the way the line turns") {
auto const track{line(100.0, 120.0, 200.0, 1000.0, 60.0)};
auto const [main, branch] {laid(track, 150.0, 30.0, 500.0, -1)};
auto const report{switch_curve::check(main, branch, track, 150.0, 180.0, 500.0, 12.0)};
CHECK(report.curved);
CHECK(report.transition);
CHECK(report.main_start == doctest::Approx(51.0 / 120.0 / 1000.0).epsilon(0.02));
CHECK(report.main_end == doctest::Approx(79.0 / 120.0 / 1000.0).epsilon(0.02));
// turning off to the outside of the curve: against it all along, the case KZW I
CHECK(report.branch_start < 0.0);
CHECK(report.branch_end < 0.0);
CHECK(false == says(report.warnings, "KZW II"));
// laid on the ramp, the cant changing under it
CHECK(report.cant_start == doctest::Approx(25.0).epsilon(0.02));
CHECK(report.cant_end == doctest::Approx(40.0).epsilon(0.02));
CHECK(says(report.warnings, "The cant changes within the switch"));
}
TEST_CASE("switch on a curve: the diverging track turning from one side to the other is the case KZW II") {
// a transition growing past the curvature of the basic switch along the switch
auto const track{line(100.0, 60.0, 100.0, 300.0, 0.0)};
auto const [main, branch] {laid(track, 125.0, 30.0, 500.0, -1)};
auto const report{switch_curve::check(main, branch, track, 125.0, 155.0, 500.0, 12.0)};
CHECK(report.branch_start < 0.0);
CHECK(report.branch_end > 0.0);
CHECK(says(report.warnings, "KZW II"));
}
TEST_CASE("switch on a curve: the ends of a cant ramp at the ends of the switch or 6 m off it") {
auto const track{line(100.0, 120.0, 200.0, 1000.0, 60.0)};
auto const ramps{switch_curve::ramp_ends(track)};
REQUIRE(ramps.size() == 2);
CHECK(ramps[0] == doctest::Approx(100.0).epsilon(0.01));
CHECK(ramps[1] == doctest::Approx(220.0).epsilon(0.01));
auto const at = [&](double const From) {
auto const [main, branch] {laid(track, From, 30.0, 500.0, 1)};
return switch_curve::check(main, branch, track, From, From + 30.0, 500.0, 12.0);
};
CHECK(says(at(210.0).warnings, "within the switch"));
CHECK(says(at(222.0).warnings, "6 m at least"));
CHECK(false == says(at(220.0).warnings, "within the switch"));
CHECK(says(at(220.0).notes, "at an end of the switch"));
CHECK(false == says(at(230.0).warnings, "cant ramp"));
// the start pressed near those places drawn to them
std::string what;
CHECK(switch_curve::snapped_start(ramps, 218.5, 30.0, 1, 3.0, &what) == doctest::Approx(220.0));
CHECK(what.find("starts at an end") != std::string::npos);
CHECK(switch_curve::snapped_start(ramps, 228.0, 30.0, 1, 3.0, &what) == doctest::Approx(226.0));
CHECK(what.find("6 m off") != std::string::npos);
CHECK(switch_curve::snapped_start(ramps, 72.0, 30.0, 1, 3.0, &what) == doctest::Approx(70.0));
CHECK(what.find("ends at an end") != std::string::npos);
CHECK(switch_curve::snapped_start(ramps, 160.0, 30.0, 1, 3.0, &what) == doctest::Approx(160.0));
CHECK(what.empty());
}
TEST_CASE("switch on a curve: one with a straight frog bent up to the frog, both tracks straight over it") {
auto const track{line(0.0, 1.0, 400.0, 1500.0, 0.0)};
for (auto const side : {1, -1}) {
CAPTURE(side);
auto [main, branch] {laid(track, 150.0, 27.0, 190.0, side)};
// the last 6 m of both straight, the diverging one with no curve of its own there either: the frog of the switch
auto const frog{21.0};
auto const straighten = [&](std::vector<switch_curve::track_point> &Track) {
auto const at{std::find_if(Track.begin(), Track.end(), [&](auto const &Point) { return Point.station >= frog; })};
for (auto point{at}; point != Track.end(); ++point)
point->heading = at->heading;
};
straighten(main);
straighten(branch);
auto const report{switch_curve::check(main, branch, track, 150.0, 177.0, 190.0, 9.0, 27.0 - frog)};
CHECK(report.curved);
// the curvature of the bent part only: no transition curve under it, the diverging track turning one way all along
CHECK_FALSE(report.transition);
CHECK(false == says(report.warnings, "KZW II"));
CHECK(says(report.warnings, "Bent up to the frog"));
}
}
TEST_CASE("switch on a curve: none of 1:9.403 nor steeper than 1:9 bent in the running and the main tracks, 1:9 itself is") {
auto const track{line(0.0, 1.0, 400.0, 1500.0, 0.0)};
auto const [main, branch] {laid(track, 150.0, 33.0, 300.0, 1)};
auto const banned = [&](double const Ratio) {
CAPTURE(Ratio);
auto const report{switch_curve::check(main, branch, track, 150.0, 183.0, 300.0, Ratio)};
REQUIRE(report.curved);
return says(report.warnings, "(11.4 pkt 4)");
};
// 1:9 of the catalogue and as read off the angle of the tracks
CHECK_FALSE(banned(9.0));
CHECK_FALSE(banned(8.998));
CHECK_FALSE(banned(9.002));
CHECK(banned(9.403));
CHECK(banned(9.41));
CHECK_FALSE(banned(9.51));
CHECK_FALSE(banned(10.0));
CHECK_FALSE(banned(12.0));
CHECK(banned(7.5));
CHECK(banned(6.6));
CHECK(banned(4.8));
}
namespace {
// a track of one curvature from its start, heading as given there, a point every half metre
std::vector<switch_curve::track_point> arc(double const Length, double const Heading, double const Curvature) {
std::vector<switch_curve::track_point> points;
for (double s{0.0}; s <= Length + 1e-9; s += 0.5)
points.push_back({s, Heading + Curvature * s, 0.0});
return points;
}
// a slip of 1:9 with the slip curves of R 190 m bent onto the line from the station on: the tracks through it with the curvature of the
// line, the slip curves with theirs plus the one of the line, one turning as the line does, the other against it
switch_curve::slip_report bent_slip(std::vector<switch_curve::track_point> const &Line, double const From, double const Curvature) {
auto const angle{std::atan(1.0 / 9.0)};
auto const half{16.6};
auto const tracks{std::vector{arc(2.0 * half, 0.0, Curvature), arc(2.0 * half, angle, Curvature * std::cos(angle))}};
auto const curves{std::vector{arc(190.0 * angle, 0.0, 1.0 / 190.0 + Curvature), arc(190.0 * angle, angle, -1.0 / 190.0 + Curvature)}};
return switch_curve::check_slip(tracks, curves, Line, From, From + 2.0 * half);
}
} // namespace
TEST_CASE("slip bent into a curve: in a side track only, its slip curve bent the way of the curve no sharper than a side track takes") {
auto const sharp{bent_slip(line(0.0, 0.0, 400.0, 800.0, 0.0), 150.0, 1.0 / 800.0)};
CHECK(sharp.curved);
CHECK(says(sharp.warnings, "11.4 pkt 4b"));
CHECK(sharp.first == doctest::Approx(1.0 / 800.0).epsilon(0.02));
CHECK(sharp.second == doctest::Approx(std::cos(std::atan(1.0 / 9.0)) / 800.0).epsilon(0.02));
REQUIRE(sharp.curves.size() == 2);
// 1/190 + 1/800: R 153.5 m, 1/190 - 1/800: R 249.2 m
CHECK(1.0 / sharp.curves[0] == doctest::Approx(153.5).epsilon(0.01));
CHECK(1.0 / sharp.curves[1] == doctest::Approx(249.2).epsilon(0.01));
CHECK(says(sharp.warnings, "R 154 m, below the least 180 m"));
CHECK(false == says(sharp.warnings, "R 249 m"));
CHECK(false == says(sharp.warnings, "9.7 pkt 12"));
// R 4000 m: the slip curve of 1/190 + 1/4000, R 181.4 m, still within the side tracks
auto const gentle{bent_slip(line(0.0, 0.0, 400.0, 4000.0, 0.0), 150.0, 1.0 / 4000.0)};
CHECK(gentle.curved);
CHECK(says(gentle.warnings, "11.4 pkt 4b"));
CHECK(false == says(gentle.warnings, "table 9.2"));
CHECK(gentle.notes.empty());
// one on a straight is no bent one
auto const straight{bent_slip(line(400.0, 0.0, 0.0, 1000.0, 0.0), 150.0, 0.0)};
CHECK_FALSE(straight.curved);
CHECK(straight.warnings.empty());
}
TEST_CASE("slip bent into a curve: the cant of the side track, a track through it turning from one side to the other") {
// the cant the same over it, none recommended in a side track though
auto const canted{bent_slip(line(0.0, 0.0, 400.0, 1000.0, 30.0), 150.0, 1.0 / 1000.0)};
CHECK(canted.cant_start == doctest::Approx(30.0));
CHECK(canted.cant_end == doctest::Approx(30.0));
CHECK(false == says(canted.warnings, "The cant changes"));
CHECK(says(canted.notes, "9.3 pkt 5"));
// a reverse curve under it
auto const curve{line(0.0, 0.0, 400.0, 1000.0, 0.0)};
auto first{arc(33.2, 0.0, 1.0 / 1000.0)};
for (auto &point : first)
if (point.station > 16.6)
point.heading = 16.6 / 1000.0 - (point.station - 16.6) / 1000.0;
auto const angle{std::atan(1.0 / 9.0)};
auto const report{switch_curve::check_slip({first, arc(33.2, angle, 1.0 / 1000.0)}, {arc(21.0, 0.0, 1.0 / 190.0)}, curve, 150.0, 183.2)};
CHECK(says(report.warnings, "9.7 pkt 12"));
}
TEST_CASE("switch exit: a curve right behind a straight frog, its sudden changes of the cant deficiency apart or together") {
double const sw{1.0 / 190.0};
// R 190 on behind a frog straight for 6.09 m, at 40 km/h: apart, 99 mm at the end, within the 130 mm of table 9.15
auto const same{switch_curve::check_exit(sw, 6.092, {{40.0, sw, sw}}, 40.0)};
CHECK(same.apart == doctest::Approx(6.092));
CHECK(same.element == doctest::Approx(6.0));
CHECK(same.change == doctest::Approx(99.4).epsilon(0.01));
CHECK(same.warnings.empty());
// the other way behind a frog straight of 1.66 m only: together, 199 mm, over the 130 mm
auto const reverse{switch_curve::check_exit(sw, 1.662, {{40.0, -sw, -sw}}, 40.0)};
CHECK(reverse.apart == doctest::Approx(1.662));
CHECK(reverse.change == doctest::Approx(198.8).epsilon(0.01));
REQUIRE(reverse.warnings.size() == 1);
CHECK(says(reverse.warnings, "tables 9.15, 9.16"));
// the same way behind it: together the curvature hardly changes
CHECK(switch_curve::check_exit(sw, 1.662, {{40.0, sw, sw}}, 40.0).warnings.empty());
// a straight of 5 m laid on first: 6.66 m apart, the curve the other way alone, 99 mm
auto const later{switch_curve::check_exit(sw, 1.662, {{5.0, 0.0, 0.0}, {40.0, -sw, -sw}}, 40.0)};
CHECK(later.apart == doctest::Approx(6.662));
CHECK(later.warnings.empty());
// a sharp curve of R 100 alone: 189 mm
CHECK(says(switch_curve::check_exit(sw, 6.092, {{30.0, 0.01, 0.01}}, 40.0).warnings, "table 9.15"));
// a transition out of the frog straight: no sudden change behind it
CHECK(switch_curve::check_exit(sw, 1.662, {{20.0, 0.0, -sw}, {20.0, -sw, -sw}}, 40.0).warnings.empty());
// the curve of a switch with a curved frog going on: nothing of the switch to judge
auto const on{switch_curve::check_exit(1.0 / 300.0, 0.0, {{30.0, 1.0 / 300.0, 1.0 / 300.0}}, 50.0)};
CHECK(on.apart < 0.0);
CHECK(on.warnings.empty());
}