17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-28 18:07:29 +01:00
Files
maszyna/maj0sted/tests/test_graph.cpp
maj00r 2376e4867b Editor: rebuild the plan library around a solved document, and bend turnouts onto curves
The library was a heap of half-built services - fitting, parallelism, niwelety,
an SVG writer - that never met each other. In their place is one authored
Document and one solver that lays it whole: tracks as element chains, turnouts
placed on them, and a Solution that is rebuilt from scratch every time and never
fed back into the document. What cannot be laid comes back as a diagnostic with
the number in it rather than as silence or a fitted-away error.

Turnouts are dimensioned the way the drawings dimension them. The blade follows
Koc's rys. 5.9 - the curve is theoretically tangent to the opornica at PR, the
blade starts at a with a planed nose of d, and A is where the rail stands u
thick - and a + d derived that way lands on his tablica 5.1. Bending follows
5.3.6: it rotates the triangle OBC about the centre of the turnout, so the
tangent t and the crossing angle stand and both radii follow from them. That is
Koc's (5.7) and (5.8) without coding either, and it puts a symmetrically bent
Rz 49E1-190-1:9 on the catalogue's Rlus 49E1-380,292-1:9. A rozjazd standing on
a curve is bent onto it; one on a track drawn straight can carry a bend of its
own. The szyny laczace take up the difference, because the switch and the frog
are only bent, not re-cut.

The plan panel draws all of it: the catalogue drawing of a type with its
construction points, the theoretical arc dashed where no rail runs, and the
blade nose as the straight it is cut as. Dragging a straight now stops where it
would pull the track out from under a turnout.

File format goes to m0s 6. Versions 3 to 5 still open.
2026-08-29 19:32:46 +02:00

226 lines
8.4 KiB
C++

// 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.
#include <cmath>
#include "check.hpp"
#include "maj0sted/editor/layout.hpp"
using namespace maj0sted::editor;
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();
}