mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 09:49:19 +02:00
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.
312 lines
13 KiB
C++
312 lines
13 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 <variant>
|
|
|
|
#include <cmath>
|
|
#include <string>
|
|
#include "check.hpp"
|
|
import eu07.editor.plan_layout;
|
|
import eu07.editor.plan_document_io;
|
|
|
|
// The .m0s format, version 5: everything by id, an explicit version, and a
|
|
// round trip that is byte-identical. Older files still open: a version-3 one has
|
|
// its arcs and blade fields converted to the piece list on the way in, and a
|
|
// version-4 one simply had no nose on its blade yet.
|
|
|
|
|
|
|
|
using namespace editor::plan;
|
|
|
|
namespace {
|
|
|
|
Document sample() {
|
|
Document document;
|
|
|
|
TurnoutType type;
|
|
type.name = "Rz 1:9 R190 z iglicą nagiętą"; // spaces and non-ASCII on purpose
|
|
type.crossing_n = 9.0;
|
|
type.length = 27.1;
|
|
type.pieces = {
|
|
TurnoutPieceSpec{0, 2.42, 0.0, 0.0, 0.0},
|
|
TurnoutPieceSpec{1, 8.3, 190.0, 190.0, 0.0058},
|
|
TurnoutPieceSpec{2, 12.75, 190.0, 250.0, 0.0},
|
|
TurnoutPieceSpec{3, 4.125, 0.0, 0.0, 0.0},
|
|
};
|
|
type.blade.tip_thickness = 0.005;
|
|
type.blade.nose = 0.125;
|
|
type.blade.railtop_width = 0.07;
|
|
document.turnout_types.push_back(type);
|
|
|
|
Track through;
|
|
through.id = mint_track(document);
|
|
through.name = "tor zasadniczy 1";
|
|
through.anchor = AtPose{100.5, -2.25, 0.7853981633974483};
|
|
through.elements = {
|
|
Element{mint_element(document), Kind::Line, 0.0, 0, 412.5, Free{}},
|
|
Element{mint_element(document), Kind::Clothoid, 600.0, -1, 40.0, Free{}},
|
|
Element{mint_element(document), Kind::Arc, 600.0, -1, 235.125, Free{}},
|
|
Element{mint_element(document), Kind::Line, 0.0, 0, 180.0, Free{}},
|
|
};
|
|
document.tracks.push_back(through);
|
|
|
|
TurnoutPlacement placement;
|
|
placement.id = mint_turnout(document);
|
|
placement.type = type.name;
|
|
placement.on = through.id;
|
|
placement.station = 312.75;
|
|
placement.hand = -1;
|
|
placement.facing = false;
|
|
placement.bend_from_track = false;
|
|
placement.bend = -1.0 / 380.292; // wygięty dwustronnie, na własny łuk
|
|
document.turnouts.push_back(placement);
|
|
|
|
Track branch;
|
|
branch.id = mint_track(document);
|
|
branch.name = "odnoga";
|
|
branch.anchor = AtPort{placement.id, Port::Frog};
|
|
branch.elements = {
|
|
Element{mint_element(document), Kind::Arc, 190.0, -1, 45.5, Free{}},
|
|
Element{mint_element(document), Kind::Line, 0.0, 0, 123.5, Free{}},
|
|
};
|
|
// Held parallel to the through track's last straight, at międzytorze.
|
|
branch.elements[1].hold = Parallel{through.elements[3].id, 4.75};
|
|
document.tracks.push_back(branch);
|
|
|
|
document.view_x = 500.0;
|
|
document.view_y = 250.0;
|
|
document.view_extent = 2000.0;
|
|
document.origin_set = true;
|
|
document.georeferenced = true;
|
|
document.origin_x = 6540000.25;
|
|
document.origin_y = 5760000.5;
|
|
// an absolute path into another MaSzyna, spaces and all
|
|
document.scn_path = "/home/kto to/MaSzyna/scenery/plan export.scn";
|
|
return document;
|
|
}
|
|
|
|
bool same(const Document& a, const Document& b) {
|
|
if (a.tracks.size() != b.tracks.size() ||
|
|
a.turnouts.size() != b.turnouts.size() ||
|
|
a.turnout_types.size() != b.turnout_types.size() || a.next_id != b.next_id) {
|
|
return false;
|
|
}
|
|
for (std::size_t i = 0; i < a.tracks.size(); ++i) {
|
|
if (a.tracks[i].id != b.tracks[i].id || a.tracks[i].name != b.tracks[i].name ||
|
|
a.tracks[i].elements.size() != b.tracks[i].elements.size()) {
|
|
return false;
|
|
}
|
|
if (a.tracks[i].anchor.index() != b.tracks[i].anchor.index()) {
|
|
return false;
|
|
}
|
|
for (std::size_t k = 0; k < a.tracks[i].elements.size(); ++k) {
|
|
const Element& x = a.tracks[i].elements[k];
|
|
const Element& y = b.tracks[i].elements[k];
|
|
if (x.id != y.id || x.kind != y.kind || x.radius != y.radius ||
|
|
x.hand != y.hand || x.length != y.length ||
|
|
x.hold.index() != y.hold.index()) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void round_trip_is_byte_identical() {
|
|
const Document document = sample();
|
|
const std::string text = editor::plan::io::serialize(document);
|
|
|
|
const auto reloaded = editor::plan::io::deserialize(text);
|
|
CHECK(reloaded.has_value());
|
|
if (!reloaded) {
|
|
return;
|
|
}
|
|
CHECK(same(document, *reloaded));
|
|
CHECK(editor::plan::io::serialize(*reloaded) == text);
|
|
}
|
|
|
|
void every_field_survives() {
|
|
const Document document = sample();
|
|
const auto reloaded = editor::plan::io::deserialize(editor::plan::io::serialize(document));
|
|
CHECK(reloaded.has_value());
|
|
if (!reloaded) {
|
|
return;
|
|
}
|
|
|
|
// A name with spaces and non-ASCII.
|
|
CHECK(reloaded->turnout_types[0].name == "Rz 1:9 R190 z iglicą nagiętą");
|
|
// The geometry list, every piece of it, and the blade's own dimensions.
|
|
CHECK(reloaded->turnout_types[0].pieces.size() == 4);
|
|
CHECK(reloaded->turnout_types[0].pieces[0].part == 0);
|
|
CHECK(reloaded->turnout_types[0].pieces[0].length == 2.42);
|
|
CHECK(reloaded->turnout_types[0].pieces[1].part == 1);
|
|
CHECK(reloaded->turnout_types[0].pieces[1].radius_start == 190.0);
|
|
CHECK(reloaded->turnout_types[0].pieces[1].turn_in == 0.0058);
|
|
CHECK(reloaded->turnout_types[0].pieces[2].radius_end == 250.0);
|
|
CHECK(reloaded->turnout_types[0].pieces[3].part == 3);
|
|
CHECK(reloaded->turnout_types[0].blade.tip_thickness == 0.005);
|
|
CHECK(reloaded->turnout_types[0].blade.nose == 0.125);
|
|
CHECK(reloaded->turnout_types[0].blade.railtop_width == 0.07);
|
|
// Placement, including a backward-facing turnout.
|
|
CHECK(reloaded->turnouts[0].station == 312.75);
|
|
CHECK(reloaded->turnouts[0].hand == -1);
|
|
CHECK(reloaded->turnouts[0].facing == false);
|
|
CHECK(reloaded->turnouts[0].bend_from_track == false);
|
|
CHECK(std::abs(reloaded->turnouts[0].bend + 1.0 / 380.292) < 1e-12);
|
|
CHECK(reloaded->turnouts[0].type == "Rz 1:9 R190 z iglicą nagiętą");
|
|
// Every element says its own shape and its own length.
|
|
CHECK(reloaded->tracks[0].elements[0].kind == Kind::Line);
|
|
CHECK(reloaded->tracks[0].elements[0].length == 412.5);
|
|
CHECK(reloaded->tracks[0].elements[1].kind == Kind::Clothoid);
|
|
CHECK(reloaded->tracks[0].elements[1].radius == 600.0);
|
|
CHECK(reloaded->tracks[0].elements[2].length == 235.125);
|
|
CHECK(reloaded->tracks[0].elements[2].hand == -1);
|
|
// A hold, and the one element that carries it.
|
|
CHECK(std::holds_alternative<Free>(reloaded->tracks[1].elements[0].hold));
|
|
CHECK(std::holds_alternative<Parallel>(reloaded->tracks[1].elements[1].hold));
|
|
CHECK(std::get<Parallel>(reloaded->tracks[1].elements[1].hold).offset == 4.75);
|
|
CHECK(std::get<Parallel>(reloaded->tracks[1].elements[1].hold).ref ==
|
|
reloaded->tracks[0].elements[3].id);
|
|
// Anchors.
|
|
CHECK(std::holds_alternative<AtPose>(reloaded->tracks[0].anchor));
|
|
CHECK(std::holds_alternative<AtPort>(reloaded->tracks[1].anchor));
|
|
CHECK(std::get<AtPort>(reloaded->tracks[1].anchor).port == Port::Frog);
|
|
// Georeference and view.
|
|
CHECK(reloaded->origin_x == 6540000.25);
|
|
CHECK(reloaded->view_extent == 2000.0);
|
|
CHECK(reloaded->georeferenced);
|
|
// and where the plan is exported to, which says which MaSzyna it is drawn for
|
|
CHECK(reloaded->scn_path == "/home/kto to/MaSzyna/scenery/plan export.scn");
|
|
// Ids keep going where they left off, so nothing minted later collides.
|
|
CHECK(reloaded->next_id == document.next_id);
|
|
}
|
|
|
|
void an_older_file_is_refused() {
|
|
// Version 1 and version 2 described different models; both are refused
|
|
// outright rather than half-understood. Version 3 is not refused — it is
|
|
// converted, which an_old_file_upgrades_to_the_piece_list checks.
|
|
const std::string version_1 =
|
|
"the plan library 1\ncrs 2180\nniwelety 0\neditor 1\neniw 2 1\nename niweleta 1\n";
|
|
CHECK(!editor::plan::io::deserialize(version_1).has_value());
|
|
const std::string version_2 =
|
|
"m0s 2\nnext 9\nview 0 0 0\norigin 0 0 0 0\nlines 0\ntypes 0\n"
|
|
"tracks 0\n";
|
|
CHECK(!editor::plan::io::deserialize(version_2).has_value());
|
|
CHECK(!editor::plan::io::deserialize("").has_value());
|
|
CHECK(!editor::plan::io::deserialize("m0s 8\n").has_value());
|
|
}
|
|
|
|
void a_file_from_before_lukowanie_still_opens() {
|
|
// Version 5 placements had nothing to say about a bend, because a turnout stood
|
|
// on the track as it found it. That is what taking the bend from the track means,
|
|
// so they open as exactly that and nothing about them moves.
|
|
const std::string version_5 =
|
|
"m0s 5\nnext 5\nview 0 0 0\norigin 0 0 0 0\ntypes 0\n"
|
|
"turnouts 1\nturnout 2 1 100 1 1\ntutype Rz 1:9 R190\ntracks 0\n";
|
|
const auto document = editor::plan::io::deserialize(version_5);
|
|
CHECK(document.has_value());
|
|
if (!document) {
|
|
return;
|
|
}
|
|
CHECK(document->turnouts.size() == 1);
|
|
CHECK(document->turnouts[0].station == 100.0);
|
|
CHECK(document->turnouts[0].bend_from_track);
|
|
CHECK(document->turnouts[0].bend == 0.0);
|
|
CHECK(editor::plan::io::serialize(*document).find("m0s 7") != std::string::npos);
|
|
}
|
|
|
|
void a_file_from_before_the_export_path_still_opens() {
|
|
// Version 6 said nothing about where a plan exported to, so it opens with
|
|
// nothing said and the host falls back on its own answer.
|
|
const std::string version_6 =
|
|
"m0s 6\nnext 3\nview 0 0 0\norigin 0 0 0 0\ntypes 0\nturnouts 0\n"
|
|
"tracks 1\ntrack 1\ntkname tor\nanchor pose 0 0 0\nelements 1\n"
|
|
"element 2 line 0 0 250\n";
|
|
const auto document = editor::plan::io::deserialize(version_6);
|
|
CHECK(document.has_value());
|
|
if (!document) {
|
|
return;
|
|
}
|
|
CHECK(document->scn_path.empty());
|
|
CHECK(document->tracks.size() == 1);
|
|
CHECK(editor::plan::io::serialize(*document).find("m0s 7") != std::string::npos);
|
|
}
|
|
|
|
void a_file_from_before_the_nose_still_opens() {
|
|
// Version 4 had the piece list but no nose on the blade: the type line went
|
|
// straight from the tip thickness to the railhead width. It opens with the nose
|
|
// at nothing, which is what the type then reports against the curve it lays.
|
|
const std::string version_4 =
|
|
"m0s 4\nnext 1\nview 0 0 0\norigin 0 0 0 0\n"
|
|
"types 1\ntype 9 27.138 0.0049 0.07 1\ntseg 2 21.02 190 190 0\n"
|
|
"tname Rz 1:9 R190\nturnouts 0\ntracks 0\n";
|
|
const auto document = editor::plan::io::deserialize(version_4);
|
|
CHECK(document.has_value());
|
|
if (!document) {
|
|
return;
|
|
}
|
|
CHECK(document->turnout_types.size() == 1);
|
|
CHECK(document->turnout_types[0].blade.tip_thickness == 0.0049);
|
|
CHECK(document->turnout_types[0].blade.nose == 0.0);
|
|
CHECK(document->turnout_types[0].blade.railtop_width == 0.07);
|
|
CHECK(document->turnout_types[0].pieces.size() == 1);
|
|
|
|
// and it is written back in the new form, with a place for the nose
|
|
CHECK(editor::plan::io::serialize(*document).find("m0s 7") != std::string::npos);
|
|
}
|
|
|
|
void an_old_file_upgrades_to_the_piece_list() {
|
|
// A version-3 catalogue: one arc, no blade, and a closing arc that was worked
|
|
// out on the fly. It has to come back as an explicit list that lays the very
|
|
// same shape — on the crossing angle and on the catalogue length.
|
|
const std::string version_3 =
|
|
"m0s 3\nnext 1\nview 0 0 0\norigin 0 0 0 0\n"
|
|
"types 1\ntype 9 0 0 27.138 0 0 0 1\ntarc 190 0 0\ntname Rz 1:9 R190\n"
|
|
"turnouts 0\ntracks 0\n";
|
|
const auto document = editor::plan::io::deserialize(version_3);
|
|
CHECK(document.has_value());
|
|
if (!document) {
|
|
return;
|
|
}
|
|
CHECK(document->turnout_types.size() == 1);
|
|
const TurnoutType& type = document->turnout_types[0];
|
|
CHECK(type.name == "Rz 1:9 R190");
|
|
CHECK(type.pieces.size() == 2); // the closing arc and the frog rail
|
|
CHECK(type.pieces[0].part == 2);
|
|
CHECK(std::abs(type.pieces[0].length - 190.0 * std::atan(1.0 / 9.0)) < 1e-9);
|
|
CHECK(type.pieces[1].part == 3);
|
|
|
|
const auto laid = editor::plan::lay_turnout(
|
|
editor::plan::geometry::Pose{0.0, 0.0, 1.0, 0.0}, to_domain(type, 1));
|
|
CHECK(laid.valid);
|
|
CHECK(std::abs(laid.angle_residual) < 1e-9);
|
|
CHECK(std::abs(laid.length_residual) < 1e-9);
|
|
|
|
// and it is written back in the new form
|
|
CHECK(editor::plan::io::serialize(*document).find("tseg ") != std::string::npos);
|
|
}
|
|
|
|
void a_truncated_file_is_refused() {
|
|
const std::string text = editor::plan::io::serialize(sample());
|
|
// Cut off half way: the counts no longer match what follows.
|
|
CHECK(!editor::plan::io::deserialize(text.substr(0, text.size() / 2)).has_value());
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
RUN(round_trip_is_byte_identical);
|
|
RUN(every_field_survives);
|
|
RUN(an_older_file_is_refused);
|
|
RUN(an_old_file_upgrades_to_the_piece_list);
|
|
RUN(a_file_from_before_the_nose_still_opens);
|
|
RUN(a_file_from_before_lukowanie_still_opens);
|
|
RUN(a_truncated_file_is_refused);
|
|
return REPORT();
|
|
}
|