17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 06:19:19 +02:00
Files
maszyna/maj0sted/tests/test_web.cpp
maj00r a083269867 Vendor the maj0sted track layout library and link it into the editor
Drop-in copy of C:\Users\maj00r\Projects\maj0sted-master minus its web/
directory - the WASM frontend is not being ported, the editor drives the
same C++ API natively. What is left is plain C++20 with no dependencies:
domain model (niweleta, plan and profile geometry, fitting, parallelism),
project serialization, rail sampling and the editor document.

src/web/ribbon.cpp was missing from the library source list; added, since
without it the ribbon symbols never make it into the archive.
2026-08-27 19:36:55 +02:00

230 lines
7.9 KiB
C++

#include <cmath>
#include <numbers>
#include "check.hpp"
#include "maj0sted/web/editor.hpp"
#include "maj0sted/web/solve.hpp"
using namespace maj0sted::web;
namespace {
bool has_kind(const std::vector<WebPolyline>& polylines, int kind) {
for (const auto& p : polylines) {
if (p.kind == kind) return true;
}
return false;
}
void arc_with_transitions_produces_full_chain() {
SolveRequest r{};
r.ax = 0.0; r.ay = 0.0;
r.bx = 300.0; r.by = 0.0;
r.cx = 300.0; r.cy = 300.0;
r.mode = 1;
r.radius = 80.0;
r.transition = 20.0;
const auto polylines = solve_scene(r);
CHECK(!polylines.empty());
CHECK(has_kind(polylines, 0)); // straight
CHECK(has_kind(polylines, 1)); // arc
CHECK(has_kind(polylines, 2)); // transition
// Each polyline has at least two points.
for (const auto& p : polylines) CHECK(p.points.size() >= 2);
}
void compound_mode_produces_two_arcs() {
SolveRequest r{};
r.ax = 0.0; r.ay = 0.0;
r.bx = 300.0; r.by = 0.0;
r.cx = 300.0; r.cy = 300.0;
r.mode = 2;
r.r1 = 120.0; r.arc1_len = 60.0; r.between = 25.0; r.r2 = 80.0;
r.entry_t = 30.0; r.exit_t = 30.0;
const auto polylines = solve_scene(r);
int arcs = 0;
for (const auto& p : polylines) {
if (p.kind == 1) ++arcs;
}
CHECK(arcs == 2);
CHECK(has_kind(polylines, 2)); // transitions present
}
void degenerate_input_falls_back_to_straights() {
// Collinear vertices -> no fit; still returns the straights, never throws.
SolveRequest r{};
r.ax = 0.0; r.ay = 0.0;
r.bx = 100.0; r.by = 0.0;
r.cx = 200.0; r.cy = 0.0;
r.mode = 0;
r.radius = 50.0;
const auto polylines = solve_scene(r);
CHECK(!polylines.empty());
CHECK(has_kind(polylines, 0)); // straights
CHECK(!has_kind(polylines, 1)); // no arc
}
NiweletaSpec short_corner(double radius, double transition = 0.0) {
NiweletaSpec spec;
spec.straights = {
StraightSpec{0.0, 0.0, 10.0, 0.0},
StraightSpec{10.0, 0.0, 10.0, 10.0},
};
spec.fits = {GapFit{.gap = 0,
.mode = transition > 0.0 ? 2 : 1,
.radius = radius,
.transition = transition}};
return spec;
}
void editor_clamps_oversized_arc_to_fitting_radius() {
const auto solved = solve_project({short_corner(60.0)});
CHECK(solved.size() == 1);
CHECK(solved[0].applied_fits.size() == 1);
const double applied = solved[0].applied_fits[0].radius;
CHECK(applied > 9.9);
CHECK(applied < 10.1);
}
void editor_leaves_gap_unfitted_when_radius_below_floor() {
// A radius too small for the requested transitions is NEVER enlarged (the
// fitting rule only ever reduces). When nothing at or below the request fits,
// the gap is simply left unconnected.
NiweletaSpec spec;
spec.straights = {
StraightSpec{0.0, 0.0, 100.0, 0.0},
StraightSpec{100.0, 0.0, 100.0, 100.0},
};
spec.fits = {GapFit{.gap = 0, .mode = 2, .radius = 1.0, .transition = 20.0}};
const auto solved = solve_project({spec});
CHECK(solved.size() == 1);
CHECK(solved[0].applied_fits.empty());
CHECK(!has_kind(solved[0].polylines, 1)); // no arc rendered
}
void editor_emits_two_rails_at_standard_gauge() {
const auto solved = solve_project({short_corner(5.0)});
CHECK(solved[0].polylines.size() == 6);
CHECK(solved[0].rendered_straights.size() == 2);
CHECK(std::abs(solved[0].rendered_straights[0].x2 - 5.0) < 1e-6);
CHECK(std::abs(solved[0].rendered_straights[1].y1 - 5.0) < 1e-6);
const auto& left = solved[0].polylines[0].points;
const auto& right = solved[0].polylines[1].points;
CHECK(left.size() == right.size());
CHECK(std::abs(std::hypot(left[0].x - right[0].x, left[0].y - right[0].y) -
1.5) < 1e-9);
CHECK(solved[0].polylines[0].straight_index == 0);
CHECK(solved[0].polylines[0].gap == -1);
CHECK(std::abs(solved[0].polylines[0].length - 5.0) < 1e-9);
const WebPolyline* arc = nullptr;
for (const auto& polyline : solved[0].polylines) {
if (polyline.kind == 1) {
arc = &polyline;
break;
}
}
CHECK(arc != nullptr);
CHECK(arc->straight_index == -1);
CHECK(arc->gap == 0);
CHECK(arc->element_index == 0);
CHECK(std::abs(arc->radius_start - 5.0) < 1e-9);
CHECK(std::abs(arc->radius_end - 5.0) < 1e-9);
CHECK(std::abs(arc->length - 5.0 * std::numbers::pi / 2.0) < 1e-9);
}
void editor_exposes_transition_radius_range() {
const auto solved = solve_project({short_corner(5.0, 2.0)});
const WebPolyline* transition = nullptr;
for (const auto& polyline : solved[0].polylines) {
if (polyline.kind == 2) {
transition = &polyline;
break;
}
}
CHECK(transition != nullptr);
CHECK(transition->gap == 0);
CHECK(std::abs(transition->length - 2.0) < 1e-9);
CHECK(transition->radius_start == 0.0);
CHECK(std::abs(transition->radius_end - 5.0) < 1e-9);
}
void editor_leaves_parallel_offset_unfitted() {
// Two parallel, laterally offset straights admit no plain arc (a single arc
// cannot connect parallels) and reverse S-curves are never substituted, so the
// gap stays unconnected rather than inventing a curve.
NiweletaSpec spec;
spec.straights = {
StraightSpec{0.0, 0.0, 200.0, 0.0},
StraightSpec{0.0, 20.0, 200.0, 20.0},
};
spec.fits = {GapFit{.gap = 0, .mode = 1, .radius = 50.0}};
const auto solved = solve_project({spec});
CHECK(solved[0].applied_fits.empty());
CHECK(!has_kind(solved[0].polylines, 1)); // no arc, no reverse pair
}
void editor_honours_requested_radius_via_single_arc() {
// A requested radius that lands on the straights (extending them toward the
// corner if needed) is used verbatim as a single arc — never enlarged and
// never swapped for a reverse pair.
const double heading = std::numbers::pi / 6.0;
const double tangent_x = 86.9615242271;
const double tangent_y = 25.9807621135;
const double dx = std::cos(heading);
const double dy = std::sin(heading);
NiweletaSpec spec;
spec.straights = {
StraightSpec{0.0, 0.0, 100.0, 0.0},
StraightSpec{tangent_x - 30.0 * dx, tangent_y - 30.0 * dy,
tangent_x + 70.0 * dx, tangent_y + 70.0 * dy},
};
spec.fits = {GapFit{.gap = 0, .mode = 1, .radius = 30.0}};
const auto solved = solve_project({spec});
CHECK(solved[0].applied_fits.size() == 1);
CHECK(std::abs(solved[0].applied_fits[0].radius - 30.0) < 1e-9);
int arc_rails = 0;
for (const auto& polyline : solved[0].polylines) {
if (polyline.kind == 1) ++arc_rails;
}
CHECK(arc_rails == 2); // a single arc (two rails), not a reverse pair
}
void hidden_tangent_guide_fits_curve_without_rendering_track() {
NiweletaSpec spec = short_corner(5.0);
spec.straights[1].hidden = true;
const auto solved = solve_project({spec});
CHECK(solved[0].applied_fits.size() == 1);
CHECK(solved[0].rendered_straights[1].hidden);
int straight_rails = 0;
int arc_rails = 0;
for (const auto& polyline : solved[0].polylines) {
if (polyline.kind == 0) ++straight_rails;
if (polyline.kind == 1) ++arc_rails;
}
CHECK(straight_rails == 2); // only the visible entry straight
CHECK(arc_rails == 2);
}
} // namespace
int main() {
RUN(arc_with_transitions_produces_full_chain);
RUN(compound_mode_produces_two_arcs);
RUN(degenerate_input_falls_back_to_straights);
RUN(editor_clamps_oversized_arc_to_fitting_radius);
RUN(editor_leaves_gap_unfitted_when_radius_below_floor);
RUN(editor_emits_two_rails_at_standard_gauge);
RUN(editor_exposes_transition_radius_range);
RUN(editor_leaves_parallel_offset_unfitted);
RUN(editor_honours_requested_radius_via_single_arc);
RUN(hidden_tangent_guide_fits_curve_without_rendering_track);
return REPORT();
}