mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-18 14:59:19 +02:00
Pinning previously rewrote only the branch's first (leaving) straight to start at the frog and run along the frog heading. Any straight appended behind it stayed where the user drew it, so its gap fit was solved against a leaving straight that had jumped out from under it — the appended fragment could end up ~1.4 km from the frog. Move the branch as one rigid body instead: the leaving straight fixes the rotation+translation onto the frog, and every appended straight rides along under the same motion, so the shapes and inter-fragment fits are preserved and the branch still meets the switch tangentially by construction. Add a test that draws an L-shaped branch far from the frog and checks the whole centreline lands next to the frog after pinning.
272 lines
9.6 KiB
C++
272 lines
9.6 KiB
C++
#include <cmath>
|
|
#include <numbers>
|
|
|
|
#include "check.hpp"
|
|
#include "maj0sted/editor/editor.hpp"
|
|
#include "maj0sted/editor/solve.hpp"
|
|
|
|
using namespace maj0sted::editor;
|
|
|
|
namespace {
|
|
|
|
bool has_kind(const std::vector<PlanPolyline>& 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 PlanPolyline* 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 PlanPolyline* 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);
|
|
}
|
|
|
|
void branch_rides_rigidly_when_pinned_to_frog() {
|
|
// Through track along +x; a switch diverges left at station 100.
|
|
NiweletaSpec through;
|
|
through.straights = {StraightSpec{0.0, 0.0, 200.0, 0.0}};
|
|
|
|
// Branch drawn far away as an L: a leaving straight plus one appended straight
|
|
// joined by an arc. Pinning must move the WHOLE branch rigidly onto the frog,
|
|
// so the appended fragment rides along instead of being left ~1.4 km away
|
|
// (the pre-fix behaviour, which only relocated the leaving straight).
|
|
NiweletaSpec branch;
|
|
branch.straights = {StraightSpec{1000.0, 1000.0, 1090.0, 1000.0},
|
|
StraightSpec{1100.0, 1000.0, 1100.0, 1090.0}};
|
|
branch.fits = {GapFit{.gap = 0, .mode = 1, .radius = 20.0}};
|
|
|
|
Junction j{};
|
|
j.through = 0;
|
|
j.station = 100.0;
|
|
j.side = 0; // diverge left
|
|
j.facing = true;
|
|
j.crossing_n = 9.0;
|
|
j.curve.mode = 1;
|
|
j.curve.radius = 190.0;
|
|
j.branch = 1;
|
|
|
|
const auto solution = solve_layout({through, branch}, {j});
|
|
CHECK(solution.junctions.size() == 1);
|
|
CHECK(solution.junctions[0].valid);
|
|
CHECK(solution.niwelety.size() == 2);
|
|
|
|
const auto& g = solution.junctions[0];
|
|
const auto& branch_solved = solution.niwelety[1];
|
|
CHECK(!branch_solved.centreline.empty());
|
|
// The branch starts exactly at the frog...
|
|
CHECK(std::hypot(branch_solved.centreline.front().x - g.fx,
|
|
branch_solved.centreline.front().y - g.fy) < 1e-6);
|
|
// ...and the appended fragment rode along: nothing sits far from the frog.
|
|
for (const auto& p : branch_solved.centreline) {
|
|
CHECK(std::hypot(p.x - g.fx, p.y - g.fy) < 300.0);
|
|
}
|
|
}
|
|
|
|
} // 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);
|
|
RUN(branch_rides_rigidly_when_pinned_to_frog);
|
|
return REPORT();
|
|
}
|