mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-18 16:09:19 +02:00
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.
This commit is contained in:
234
editor/tests/test_graph.cpp
Normal file
234
editor/tests/test_graph.cpp
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
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 "check.hpp"
|
||||
import eu07.editor.plan_layout;
|
||||
|
||||
// What refers to what decides the order things are laid in. A turnout on the
|
||||
// branch of a turnout is not a special case, and a reference that runs in a
|
||||
// circle is reported rather than relaxed at for a fixed number of passes.
|
||||
|
||||
|
||||
|
||||
using namespace editor::plan;
|
||||
|
||||
namespace {
|
||||
|
||||
bool near(double a, double b, double tolerance = 1e-6) {
|
||||
return std::abs(a - b) <= tolerance;
|
||||
}
|
||||
|
||||
bool has(const Solution& solution, Code code) {
|
||||
for (const auto& diagnostic : solution.diagnostics) {
|
||||
if (diagnostic.code == code) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// A plain Rz 1:9 R190, the everyday Polish turnout.
|
||||
void add_catalogue(Document& document) {
|
||||
TurnoutType type;
|
||||
type.name = "Rz 1:9 R190";
|
||||
type.crossing_n = 9.0;
|
||||
type.length = 27.138; // the catalogue's a + b
|
||||
type.blade = BladeSpec{0.005, 0.070};
|
||||
type.pieces = {
|
||||
// przediglicowy, then blade and curve as one arc of the crossing angle, then
|
||||
// the frog rail: a = 13,834 and b = 13,304 with R*tan(alfa/2) = 10,5232
|
||||
TurnoutPieceSpec{0, 3.3108, 0.0, 0.0, 0.0},
|
||||
TurnoutPieceSpec{1, 7.188, 190.0, 190.0, 0.0},
|
||||
TurnoutPieceSpec{2, 190.0 * std::atan(1.0 / 9.0) - 7.188, 190.0, 190.0, 0.0},
|
||||
TurnoutPieceSpec{3, 2.7808, 0.0, 0.0, 0.0},
|
||||
};
|
||||
document.turnout_types.push_back(type);
|
||||
}
|
||||
|
||||
TurnoutId add_turnout(Document& document, TrackId on, double station, int hand) {
|
||||
TurnoutPlacement placement;
|
||||
placement.id = mint_turnout(document);
|
||||
placement.type = "Rz 1:9 R190";
|
||||
placement.on = on;
|
||||
placement.station = station;
|
||||
placement.hand = hand;
|
||||
document.turnouts.push_back(placement);
|
||||
return placement.id;
|
||||
}
|
||||
|
||||
TrackId add_straight_track(Document& document, const char* name, double x, double y,
|
||||
double azimuth, double length) {
|
||||
Track track;
|
||||
track.id = mint_track(document);
|
||||
track.name = name;
|
||||
track.anchor = AtPose{x, y, azimuth};
|
||||
track.elements = {
|
||||
Element{mint_element(document), Kind::Line, 0.0, 0, length, Free{}}};
|
||||
document.tracks.push_back(track);
|
||||
return track.id;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// A branch anchored to a frog is laid after the turnout, which is laid after
|
||||
/// its through track — whatever order they appear in the document.
|
||||
void a_branch_follows_its_frog() {
|
||||
Document document;
|
||||
add_catalogue(document);
|
||||
|
||||
// The branch is declared BEFORE the turnout and the through track it needs.
|
||||
Track branch;
|
||||
branch.id = mint_track(document);
|
||||
branch.name = "odnoga";
|
||||
branch.elements = {
|
||||
Element{mint_element(document), Kind::Line, 0.0, 0, 100.0, Free{}}};
|
||||
document.tracks.push_back(branch);
|
||||
|
||||
const TrackId through = add_straight_track(document, "zasadniczy", 0.0, 0.0, 0.0, 1000.0);
|
||||
const TurnoutId turnout = add_turnout(document, through, 300.0, 1);
|
||||
document.tracks[0].anchor = AtPort{turnout, Port::Frog};
|
||||
|
||||
const Solution solution = solve(document);
|
||||
|
||||
CHECK(solution.diagnostics.empty());
|
||||
const SolvedTurnout* solved_turnout = find_turnout(solution, turnout);
|
||||
CHECK(solved_turnout != nullptr && solved_turnout->valid);
|
||||
|
||||
const SolvedTrack* solved_branch = find_track(solution, branch.id);
|
||||
CHECK(solved_branch != nullptr);
|
||||
CHECK(solved_branch->elements.size() == 1);
|
||||
// The branch starts exactly at the frog, on the frog's heading. It was never
|
||||
// anywhere else, so there is nothing to pin back.
|
||||
CHECK(near(solved_branch->start.x, solved_turnout->frog.x));
|
||||
CHECK(near(solved_branch->start.y, solved_turnout->frog.y));
|
||||
CHECK(near(solved_branch->start.hx, solved_turnout->frog.hx));
|
||||
CHECK(near(solved_branch->start.hy, solved_turnout->frog.hy));
|
||||
// It diverges: the frog is off the through track's straight line.
|
||||
CHECK(std::abs(solved_turnout->frog.x) > 0.5);
|
||||
}
|
||||
|
||||
/// A turnout on the branch of a turnout. The old solver ran a fixed two passes,
|
||||
/// so this depth had no way to settle.
|
||||
void a_turnout_on_a_branch_of_a_turnout() {
|
||||
Document document;
|
||||
add_catalogue(document);
|
||||
|
||||
const TrackId through = add_straight_track(document, "zasadniczy", 0.0, 0.0, 0.0, 2000.0);
|
||||
const TurnoutId first = add_turnout(document, through, 300.0, 1);
|
||||
|
||||
Track branch;
|
||||
branch.id = mint_track(document);
|
||||
branch.name = "odnoga 1";
|
||||
branch.anchor = AtPort{first, Port::Frog};
|
||||
branch.elements = {
|
||||
Element{mint_element(document), Kind::Line, 0.0, 0, 400.0, Free{}}};
|
||||
document.tracks.push_back(branch);
|
||||
|
||||
const TurnoutId second = add_turnout(document, branch.id, 150.0, -1);
|
||||
|
||||
Track branch2;
|
||||
branch2.id = mint_track(document);
|
||||
branch2.name = "odnoga 2";
|
||||
branch2.anchor = AtPort{second, Port::Frog};
|
||||
branch2.elements = {
|
||||
Element{mint_element(document), Kind::Arc, 300.0, -1, 120.0, Free{}}};
|
||||
document.tracks.push_back(branch2);
|
||||
|
||||
const Solution solution = solve(document);
|
||||
|
||||
CHECK(solution.diagnostics.empty());
|
||||
CHECK(find_turnout(solution, second) != nullptr);
|
||||
CHECK(find_turnout(solution, second)->valid);
|
||||
|
||||
const SolvedTrack* deep = find_track(solution, branch2.id);
|
||||
CHECK(deep != nullptr);
|
||||
CHECK(deep->elements.size() == 1);
|
||||
CHECK(near(deep->start.x, find_turnout(solution, second)->frog.x));
|
||||
CHECK(near(deep->start.y, find_turnout(solution, second)->frog.y));
|
||||
// Three levels deep, well away from the through track.
|
||||
CHECK(std::abs(deep->start.x) > 1.0);
|
||||
}
|
||||
|
||||
/// References that run in a circle are named, not relaxed at.
|
||||
void a_cycle_is_reported() {
|
||||
// Two tracks, each held parallel to the other's element.
|
||||
Document document;
|
||||
const TrackId first = add_straight_track(document, "tor 1", 0.0, 0.0, 0.0, 500.0);
|
||||
const TrackId second = add_straight_track(document, "tor 2", 20.0, 0.0, 0.0, 500.0);
|
||||
document.tracks[0].elements[0].hold =
|
||||
Parallel{document.tracks[1].elements[0].id, 4.75};
|
||||
document.tracks[1].elements[0].hold =
|
||||
Parallel{document.tracks[0].elements[0].id, 4.75};
|
||||
(void)first;
|
||||
(void)second;
|
||||
|
||||
const Solution solution = solve(document);
|
||||
CHECK(has(solution, Code::CyclicDependency));
|
||||
}
|
||||
|
||||
/// A turnout whose catalogue type is missing says so, and the branch that hangs
|
||||
/// off it says why it could not be laid — neither is silently dropped.
|
||||
void a_missing_type_is_reported() {
|
||||
Document document;
|
||||
const TrackId through = add_straight_track(document, "zasadniczy", 0.0, 0.0, 0.0, 500.0);
|
||||
const TurnoutId turnout = add_turnout(document, through, 100.0, 1); // no catalogue
|
||||
|
||||
Track branch;
|
||||
branch.id = mint_track(document);
|
||||
branch.name = "odnoga";
|
||||
branch.anchor = AtPort{turnout, Port::Frog};
|
||||
branch.elements = {
|
||||
Element{mint_element(document), Kind::Line, 0.0, 0, 50.0, Free{}}};
|
||||
document.tracks.push_back(branch);
|
||||
|
||||
const Solution solution = solve(document);
|
||||
CHECK(has(solution, Code::UnknownReference));
|
||||
CHECK(solution.diagnostics.size() >= 2);
|
||||
// Every track still has an entry, so a host can index by id without holes.
|
||||
CHECK(find_track(solution, branch.id) != nullptr);
|
||||
CHECK(find_track(solution, through) != nullptr);
|
||||
}
|
||||
|
||||
/// Solving twice gives the same answer, and solving does not touch the document.
|
||||
void solving_leaves_the_document_alone() {
|
||||
Document document;
|
||||
add_catalogue(document);
|
||||
const TrackId through = add_straight_track(document, "zasadniczy", 0.0, 0.0, 0.0, 1000.0);
|
||||
const TurnoutId turnout = add_turnout(document, through, 300.0, 1);
|
||||
Track branch;
|
||||
branch.id = mint_track(document);
|
||||
branch.anchor = AtPort{turnout, Port::Frog};
|
||||
branch.elements = {
|
||||
Element{mint_element(document), Kind::Arc, 300.0, 1, 200.0, Free{}}};
|
||||
document.tracks.push_back(branch);
|
||||
|
||||
const Document before = document;
|
||||
const Solution first = solve(document);
|
||||
const Solution second = solve(document);
|
||||
|
||||
CHECK(document.tracks.size() == before.tracks.size());
|
||||
CHECK(document.next_id == before.next_id);
|
||||
CHECK(first.tracks.size() == second.tracks.size());
|
||||
for (std::size_t i = 0; i < first.tracks.size(); ++i) {
|
||||
CHECK(near(first.tracks[i].length, second.tracks[i].length));
|
||||
CHECK(near(first.tracks[i].end.x, second.tracks[i].end.x));
|
||||
CHECK(near(first.tracks[i].end.y, second.tracks[i].end.y));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
RUN(a_branch_follows_its_frog);
|
||||
RUN(a_turnout_on_a_branch_of_a_turnout);
|
||||
RUN(a_cycle_is_reported);
|
||||
RUN(a_missing_type_is_reported);
|
||||
RUN(solving_leaves_the_document_alone);
|
||||
return REPORT();
|
||||
}
|
||||
Reference in New Issue
Block a user