17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 16:49:21 +02:00
Files
maszyna/editor/tests/test_hold.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

320 lines
12 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 <cstdio>
#include <numbers>
#include "check.hpp"
import eu07.editor.plan_layout;
import eu07.editor.plan_sketch;
// What one track keeps against another, and what clicking along the ground
// works out at the click. Both are the same idea from two ends: geometry is
// decided once, in numbers, and nothing is left pointing at a drawing.
using namespace editor::plan;
namespace {
bool near(double a, double b, double tolerance = 1e-6) {
return std::abs(a - b) <= tolerance;
}
Element line(Document& document, double metres) {
return Element{mint_element(document), Kind::Line, 0.0, 0, metres, Free{}};
}
Element arc(Document& document, double radius, int hand, double metres) {
return Element{mint_element(document), Kind::Arc, radius, hand, metres, Free{}};
}
/// A straight due north from the origin, 1000 m of it.
TrackId add_main(Document& document, ElementId& out_element) {
Track track;
track.id = mint_track(document);
track.name = "tor 1";
track.anchor = AtPose{0.0, 0.0, 0.0};
track.elements = {line(document, 1000.0)};
out_element = track.elements.front().id;
document.tracks.push_back(track);
return track.id;
}
bool has(const Solution& solution, Code code) {
for (const auto& diagnostic : solution.diagnostics) {
if (diagnostic.code == code) {
return true;
}
}
return false;
}
// -------------------------------------------------------------------------
/// A corner rounded at the click: the tangent comes off both straights, and the
/// arc turns exactly as far as the corner bends.
void a_corner_is_rounded_with_a_tangent_arc() {
// Due north, then due east: a 90° corner to the right.
const Corner corner = fit_corner(0.0, 1.0, 1.0, 0.0, 300.0);
CHECK(corner.ok);
CHECK(corner.hand == -1);
CHECK(near(corner.deflection, -std::numbers::pi / 2.0));
CHECK(near(corner.tangent, 300.0)); // R·tan(45°)
CHECK(near(corner.arc_length, 300.0 * std::numbers::pi / 2.0));
// Straight on is not a corner, and neither is doubling back.
CHECK(!fit_corner(0.0, 1.0, 0.0, 1.0, 300.0).ok);
CHECK(!fit_corner(0.0, 1.0, 0.0, -1.0, 300.0).ok);
CHECK(!fit_corner(0.0, 1.0, 1.0, 0.0, 0.0).ok);
}
/// The corner really does come out tangent: laying the three elements it
/// produces lands the track on the second leg, heading along it.
void a_rounded_corner_lands_on_the_next_leg() {
Document document;
const Corner corner = fit_corner(0.0, 1.0, 1.0, 0.0, 300.0);
Track track;
track.id = mint_track(document);
track.anchor = AtPose{0.0, 0.0, 0.0};
track.elements = {line(document, 1000.0 - corner.tangent),
arc(document, 300.0, corner.hand, corner.arc_length),
line(document, 800.0 - corner.tangent)};
document.tracks.push_back(track);
const Solution solution = solve(document);
CHECK(solution.diagnostics.empty());
const SolvedTrack* laid = find_track(solution, track.id);
CHECK(laid != nullptr);
// The corner was at (0, 1000); the second leg runs east from there to (800, 1000).
CHECK(near(laid->end.x, 800.0, 1e-6));
CHECK(near(laid->end.y, 1000.0, 1e-6));
CHECK(near(laid->end.hx, 1.0, 1e-9));
CHECK(near(laid->end.hy, 0.0, 1e-9));
}
/// Międzytorze, said once: the held track is moved bodily onto the offset, and
/// stays where it was put along it.
void a_track_held_parallel_is_moved_onto_the_offset() {
Document document;
ElementId reference{ElementId::none};
add_main(document, reference);
Track siding;
siding.id = mint_track(document);
siding.name = "tor 2";
// Put it well off the offset, and part way up the main track.
siding.anchor = AtPose{40.0, 300.0, 0.0};
siding.elements = {line(document, 500.0)};
siding.elements.front().hold = Parallel{reference, 4.75};
document.tracks.push_back(siding);
const Solution solution = solve(document);
CHECK(solution.diagnostics.empty());
const SolvedTrack* laid = find_track(solution, siding.id);
CHECK(laid != nullptr && laid->elements.size() == 1);
// The main track heads north, so left of its travel is -x.
CHECK(near(laid->elements[0].start.x, -4.75, 1e-9));
// Nothing slid it along: it kept the y it was anchored at.
CHECK(near(laid->elements[0].start.y, 300.0, 1e-9));
CHECK(near(laid->elements[0].start.hy, 1.0, 1e-9));
CHECK(near(laid->length, 500.0));
// The offset is signed: the other side is the other side.
document.tracks.back().elements.front().hold = Parallel{reference, -4.75};
const Solution mirrored = solve(document);
const SolvedTrack* other = find_track(mirrored, document.tracks.back().id);
CHECK(other != nullptr && near(other->elements[0].start.x, 4.75, 1e-9));
}
/// Parallel to an arc is concentric with it: same centre, R∓d.
void a_track_held_parallel_to_an_arc_is_concentric() {
Document document;
Track main;
main.id = mint_track(document);
main.name = "tor 1";
main.anchor = AtPose{0.0, 0.0, 0.0};
main.elements = {arc(document, 500.0, 1, 400.0)};
const ElementId reference = main.elements.front().id;
document.tracks.push_back(main);
Track siding;
siding.id = mint_track(document);
siding.name = "tor 2";
siding.anchor = AtPose{-60.0, 10.0, 0.0};
// The radius and hand asked for here are beside the point: the reference
// fixes the shape.
siding.elements = {arc(document, 999.0, -1, 200.0)};
siding.elements.front().hold = Parallel{reference, 50.0};
document.tracks.push_back(siding);
const Solution solution = solve(document);
CHECK(solution.diagnostics.empty());
const SolvedTrack* laid = find_track(solution, siding.id);
CHECK(laid != nullptr && laid->elements.size() == 1);
// Left of a left-hand curve is the inside: R - d.
CHECK(near(laid->elements[0].k0, 1.0 / 450.0, 1e-12));
// The main track's centre of curvature is 500 m to the left of its start.
CHECK(near(std::hypot(laid->elements[0].start.x + 500.0, laid->elements[0].start.y),
450.0, 1e-6));
CHECK(near(laid->length, 200.0));
}
/// A hold is a reference like any other: it orders the solve, and a circle of
/// them is reported rather than relaxed at.
void holds_are_ordered_and_cycles_are_caught() {
Document document;
ElementId reference{ElementId::none};
add_main(document, reference);
// Held to a track that is itself held: laid third, and still exact.
Track second;
second.id = mint_track(document);
second.name = "tor 2";
second.anchor = AtPose{40.0, 0.0, 0.0};
second.elements = {line(document, 400.0)};
second.elements.front().hold = Parallel{reference, 4.75};
const ElementId middle = second.elements.front().id;
document.tracks.push_back(second);
Track third;
third.id = mint_track(document);
third.name = "tor 3";
third.anchor = AtPose{80.0, 0.0, 0.0};
third.elements = {line(document, 400.0)};
third.elements.front().hold = Parallel{middle, 4.75};
document.tracks.push_back(third);
const Solution solution = solve(document);
CHECK(solution.diagnostics.empty());
const SolvedTrack* laid = find_track(solution, third.id);
CHECK(laid != nullptr && near(laid->elements[0].start.x, -9.5, 1e-9));
// Now point the first one back at the last: a circle.
document.tracks[0].elements[0].hold =
Parallel{document.tracks[2].elements[0].id, 4.75};
CHECK(has(solve(document), Code::CyclicDependency));
}
/// Two elements of one movable track cannot both decide where it goes.
void two_holds_on_one_movable_track_conflict() {
Document document;
ElementId reference{ElementId::none};
add_main(document, reference);
Track siding;
siding.id = mint_track(document);
siding.name = "tor 2";
siding.anchor = AtPose{40.0, 0.0, 0.0};
siding.elements = {line(document, 200.0), line(document, 200.0)};
siding.elements[0].hold = Parallel{reference, 4.75};
siding.elements[1].hold = Parallel{reference, 9.50};
document.tracks.push_back(siding);
CHECK(has(solve(document), Code::HoldConflict));
}
/// A branch pinned to a turnout's frog cannot be moved onto the offset, so the
/// corner between the frog and the held straight takes up the difference — the
/// same fit as pulling a straight about by hand, and no full turn anywhere.
void a_pinned_branch_fits_its_way_onto_the_offset() {
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);
ElementId reference{ElementId::none};
const TrackId main = add_main(document, reference);
TurnoutPlacement placement;
placement.id = mint_turnout(document);
placement.type = type.name;
placement.on = main;
placement.station = 300.0;
placement.hand = -1; // diverging to the right of a northbound track
document.turnouts.push_back(placement);
// Off the frog: a straight, a corner, and the straight that is to run beside
// the main track. Nothing about it is right to begin with.
Track branch;
branch.id = mint_track(document);
branch.name = "bocznica";
branch.anchor = AtPort{placement.id, Port::Frog};
branch.elements = {line(document, 40.0), arc(document, 300.0, 1, 30.0),
line(document, 400.0)};
branch.elements[2].hold = Parallel{reference, -4.75};
document.tracks.push_back(branch);
const Solution solution = solve(document);
for (const auto& diagnostic : solution.diagnostics) {
std::printf(" diag: %s\n", diagnostic.text.c_str());
}
CHECK(!has(solution, Code::OffsetNotHeld));
const SolvedTrack* laid = find_track(solution, branch.id);
CHECK(laid != nullptr && laid->elements.size() == 3);
const SolvedTurnout* turnout = find_turnout(solution, placement.id);
CHECK(turnout != nullptr && turnout->valid);
// It still starts exactly on the frog: the fit moved lengths, not the branch.
CHECK(near(laid->start.x, turnout->frog.x, 1e-6));
CHECK(near(laid->start.y, turnout->frog.y, 1e-6));
// And the held straight really lies 4.75 m to the right of the main track,
// pointing the same way.
CHECK(near(laid->elements[2].start.x, 4.75, 1e-6));
CHECK(near(laid->elements[2].start.hy, 1.0, 1e-9));
// The corner took up the difference without looping the long way round.
CHECK(laid->elements[1].length < 300.0 * std::numbers::pi);
}
/// A reference that is not there is said so, and the track is still laid.
void a_missing_reference_is_reported() {
Document document;
Track siding;
siding.id = mint_track(document);
siding.name = "tor 2";
siding.anchor = AtPose{0.0, 0.0, 0.0};
siding.elements = {line(document, 100.0)};
siding.elements.front().hold = Parallel{static_cast<ElementId>(9999), 4.75};
document.tracks.push_back(siding);
const Solution solution = solve(document);
CHECK(has(solution, Code::UnknownReference));
const SolvedTrack* laid = find_track(solution, siding.id);
CHECK(laid != nullptr && laid->elements.size() == 1);
}
} // namespace
int main() {
RUN(a_corner_is_rounded_with_a_tangent_arc);
RUN(a_rounded_corner_lands_on_the_next_leg);
RUN(a_track_held_parallel_is_moved_onto_the_offset);
RUN(a_track_held_parallel_to_an_arc_is_concentric);
RUN(holds_are_ordered_and_cycles_are_caught);
RUN(two_holds_on_one_movable_track_conflict);
RUN(a_pinned_branch_fits_its_way_onto_the_offset);
RUN(a_missing_reference_is_reported);
return REPORT();
}