17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 15:39:20 +02:00
Files
maszyna/editor/tests/test_graph.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

235 lines
8.6 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 "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();
}