17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-31 20:29:04 +01:00

fix(editor): a straight between two curves turning the same way, traced in the mode of a curve, is laid as a straight: points along a curve, about 200 m of straight and a curve the same way (wgr.scn) were fitted as one compound curve of eight arcs, its straight two arcs run up to R 50000 and laid at R 10360 once the curve was scaled between its tangents. The two curves with a straight between them, fitted about as closely, weren't taken over it, and were refused anyway: the last curve rounded to the metre no longer fitted up to the last point. A compound curve with an arc more than 4 times flatter than its sharpest one gives way to curves with straights between them which keep to the points about as closely, where straights between curves may be laid; the curves fitted are settled to the metre, or as laid a hair under where that gives a warning, as the chain of a reverse curve was; the chain of curves takes curves turning the same way with straights between them too, three of them, and wins over the two curves where it keeps closer. Unit tests of the points of wgr.scn and of two and three curves turning one way with straights between them

This commit is contained in:
maj00r
2026-10-26 19:50:00 +01:00
parent d6df785b78
commit ed3d6551d8
2 changed files with 144 additions and 42 deletions

View File

@@ -3857,6 +3857,25 @@ void fit_compound(design &Design, std::size_t const Vertex, std::vector<glm::dve
Design = fitted;
}
// a compound curve of an arc this many times flatter than its sharpest one follows a straight the points show between two curves turning
// the same way, or off the straight at its end: curves with a straight between them are taken over it where they keep to the points
// about as closely
constexpr double kCompoundSpread{4.0};
bool flattened(vertex const &Curve)
{
if (false == Curve.compound || Curve.arcs.empty())
return false;
auto sharpest{Curve.radius};
auto flattest{Curve.radius};
for (auto const &arc : Curve.arcs)
{
sharpest = std::min(sharpest, arc.radius);
flattest = std::max(flattest, arc.radius);
}
return flattest > kCompoundSpread * sharpest;
}
// the one curve of the design as a compound curve of more arcs than it has, up to Most, as few as keep within kPointsWithin of the
// points, all along with the transitions of the norms between the arcs: each count started from the curve fitted with an arc less,
// its longest arc cut in two, and from the curvature the points show, the better fit taken
@@ -3977,11 +3996,42 @@ std::optional<trace_stretch> fit_arcs(design const &Single, std::vector<glm::dve
namespace
{
// the curves of the design with the radii and the transitions they were laid with, to the metre, or, Exact, as they were laid, a hair
// under: where a curve fills the room up to an end of the stretch or to the next curve, a metre more doesn't fit there, the design laid
// again warning of radii made smaller
design settled_curves(design Design, result const &Laid, bool const Exact, double const Speed)
{
for (auto const &curve : Laid.curves)
{
if (curve.vertex < 0 || curve.vertex >= static_cast<int>(Design.vertices.size()))
continue;
auto &curved{Design.vertices[curve.vertex]};
auto const radius{Exact ? curve.radius * (1.0 - 1e-6) : std::max(kTraceRadiusMin, std::round(curve.radius))};
auto const in{Exact ? curve.transition_in * (1.0 - 1e-6) : std::round(curve.transition_in)};
auto const out{Exact ? curve.transition_out * (1.0 - 1e-6) : std::round(curve.transition_out)};
plain_curve(curved, radius, std::min(in, out), Speed, Design.norms);
curved.transition_in = in;
curved.transition_out = out;
}
return Design;
}
// the same laid again: to the metre where that gives no warning, else as laid
design settled_curves(design const &Design, double const Speed)
{
auto const laid{compute(Design)};
auto rounded{settled_curves(Design, laid, false, Speed)};
if (auto const again{compute(rounded)}; again.valid && false == again.radius_warnings.empty())
return settled_curves(Design, laid, true, Speed);
return rounded;
}
// a chain of curves the points show, two to four: a curve where they turn one way, the next where they turn the other way or after a
// straight. The lines between the curves, the common tangents of two curves of a reverse curve, are fitted with the radii and the
// transitions; two curves turning the other way may meet with no straight between them, their tangents filling the room between their
// vertices. Two turning the same way are two curves only with a straight of shortest_between between them, else one compound curve
std::optional<trace_stretch> fit_chain(design const &Base, std::vector<glm::dvec2> const &Points, double const Speed)
// vertices. Two turning the same way are two curves only with a straight of shortest_between between them, else one compound curve, and
// only with Straights
std::optional<trace_stretch> fit_chain(design const &Base, std::vector<glm::dvec2> const &Points, double const Speed, bool const Straights)
{
auto const n{Points.size()};
if (n < 6)
@@ -4041,7 +4091,7 @@ std::optional<trace_stretch> fit_chain(design const &Base, std::vector<glm::dvec
else
++i;
auto const count{parts.size()};
if (count < 2 || count > 4 || std::none_of(parts.begin() + 1, parts.end(), [&](part const &Part) { return Part.side != (&Part - 1)->side; }))
if (count < 2 || count > 4 || (false == Straights && std::any_of(parts.begin() + 1, parts.end(), [&](part const &Part) { return Part.side == (&Part - 1)->side; })))
return std::nullopt;
// the parameters: of each line between two curves its direction and its distance from the start across it, then the logarithms of the
@@ -4141,37 +4191,11 @@ std::optional<trace_stretch> fit_chain(design const &Base, std::vector<glm::dvec
design chain;
if (false == shaped(best, chain))
return std::nullopt;
auto const laid{compute(chain)};
if (false == laid.valid || laid.curves.size() != count)
if (auto const laid{compute(chain)}; false == laid.valid || laid.curves.size() != count)
return std::nullopt;
// the radii and the transitions the curves were laid with, to the metre; where two curves of a reverse curve meet, filling the room
// between their vertices, a metre more doesn't fit: those as they were laid, a hair under, the design laid again giving no warning
auto const settled = [&](bool const Exact) {
auto settle{chain};
for (auto const &curve : laid.curves)
{
if (curve.vertex < 0 || curve.vertex >= static_cast<int>(count))
return std::optional<design>{};
auto &curved{settle.vertices[curve.vertex]};
auto const radius{Exact ? curve.radius * (1.0 - 1e-6) : std::max(kTraceRadiusMin, std::round(curve.radius))};
auto const in{Exact ? curve.transition_in * (1.0 - 1e-6) : std::round(curve.transition_in)};
auto const out{Exact ? curve.transition_out * (1.0 - 1e-6) : std::round(curve.transition_out)};
plain_curve(curved, radius, std::min(in, out), Speed, settle.norms);
curved.transition_in = in;
curved.transition_out = out;
}
return std::optional<design>{settle};
};
auto rounded{settled(false)};
if (false == rounded.has_value())
return std::nullopt;
chain = *rounded;
auto again{compute(chain)};
if (again.valid && false == again.radius_warnings.empty())
{
chain = *settled(true);
again = compute(chain);
}
// where two curves of a reverse curve meet, filling the room between their vertices, as they were laid
chain = settled_curves(chain, Speed);
auto const again{compute(chain)};
if (false == again.valid || again.curves.size() != count || false == again.radius_warnings.empty())
return std::nullopt;
for (auto const &curve : again.curves)
@@ -4196,11 +4220,15 @@ trace_stretch fit_trace_stretch(design const &Base, std::vector<glm::dvec2> cons
auto const startdirection{glm::normalize(Base.start_direction)};
auto const end{plan_of(Base.end)};
auto const enddirection{glm::normalize(Base.end_direction)};
// a shape with more curves is taken where it follows the points much closer than the simpler one, which doesn't keep to them
// one compound curve with an arc flattened to a straight between its curves, where straights between curves may be laid
auto const flat = [&]() { return Straights && chosen.valid && chosen.vertices.size() == 1 && flattened(chosen.vertices.front()); };
// a shape with more curves is taken where it follows the points much closer than the simpler one, which doesn't keep to them; curves
// with straights between them over such a compound curve where they keep to the points about as closely
auto const take = [&](std::optional<trace_stretch> const &Candidate) {
if (false == Candidate.has_value())
return;
if (false == chosen.valid || (chosen.rms > kStretchClose && Candidate->rms < 0.6 * chosen.rms))
if (false == chosen.valid || (chosen.rms > kStretchClose && Candidate->rms < 0.6 * chosen.rms) ||
(flat() && Candidate->vertices.size() >= 2 && Candidate->rms < std::max(kStretchClose, 2.0 * chosen.rms)))
chosen = *Candidate;
};
@@ -4295,7 +4323,7 @@ trace_stretch fit_trace_stretch(design const &Base, std::vector<glm::dvec2> cons
// the curves. The parameters: the direction of the straight, its distance from the start across it, the logarithms of the radii,
// first with the transitions of the single curve, then with the square roots of the lengths of the transitions of each curve too
constexpr std::size_t kParameters{4};
if (Straights && Points.size() > kParameters && (false == chosen.valid || chosen.rms > kStretchClose))
if (Straights && Points.size() > kParameters && (false == chosen.valid || chosen.rms > kStretchClose || flat()))
{
auto const transition{one.has_value() && false == one->vertices.empty() ? one->vertices.front().transition_in : 0.0};
auto const shaped = [&](std::vector<double> const &Parameters, design &Trial) {
@@ -4364,8 +4392,8 @@ trace_stretch fit_trace_stretch(design const &Base, std::vector<glm::dvec2> cons
}
if (design two; false == best.empty() && shaped(best, two))
{
for (auto &curve : two.vertices)
plain_curve(curve, std::max(kTraceRadiusMin, std::round(curve.radius)), std::round(curve.transition_in), speed, two.norms);
// a curve ending at the last point, filling the room up to the end, as it was laid
two = settled_curves(two, speed);
// none where a curve ran to the end of the radii tried, turns hardly at all, or doesn't fit between the straights: the
// points made the fit find two curves where they show one
auto const laid{compute(two)};
@@ -4379,14 +4407,21 @@ trace_stretch fit_trace_stretch(design const &Base, std::vector<glm::dvec2> cons
auto const candidate{sound ? stretch_of(two, Points) : std::nullopt};
auto const curves{laid_curves(laid)};
auto const same{curves.size() == 2 && curves[0].side == curves[1].side};
if (candidate.has_value() && (false == same || (curves[1].from - curves[0].to >= shortest_between(Base) && (false == chosen.valid || candidate->rms < 0.5 * chosen.rms))))
if (candidate.has_value() && (false == same || (curves[1].from - curves[0].to >= shortest_between(Base) && (false == chosen.valid || candidate->rms < 0.5 * chosen.rms || flat()))))
take(candidate);
}
}
// a reverse curve, or more curves turning to and fro
if (false == chosen.valid || chosen.rms > kStretchClose)
take(fit_chain(Base, Points, speed));
// a reverse curve, or more curves turning to and fro or the same way with straights between them; of no more curves than the two
// fitted above, the closer one
if (false == chosen.valid || chosen.rms > kStretchClose || chosen.vertices.size() >= 2 || flat())
if (auto const chain{fit_chain(Base, Points, speed, Straights)}; chain.has_value())
{
if (chosen.valid && chosen.vertices.size() >= 2 && chain->vertices.size() <= chosen.vertices.size() && chain->rms < chosen.rms)
chosen = *chain;
else
take(chain);
}
return chosen;
}

View File

@@ -882,3 +882,70 @@ TEST_CASE("alignment: reverse curves traced in the mode of a curve, with no stra
CHECK(line.radius_warnings.empty());
}
}
TEST_CASE("alignment: a straight between two curves turning the same way, traced in the mode of a curve, is laid as a straight") {
// the points clicked in a scenery, from the start of the straight before the curves on, a curve of about 550 m, about 200 m of
// straight, a curve of about 550 m turning the same way and the end; once fitted as one compound curve of eight arcs, its straight
// two arcs of R 10360
std::vector<glm::dvec2> const clicks{{-1495.4964, 300.4092}, {-1653.8092, 290.0271}, {-1680.8739, 287.7361}, {-1707.4353, 284.2790}, {-1739.5235, 278.3567}, {-1780.0393, 268.1851},
{-1803.7878, 260.7141}, {-1845.8626, 244.5926}, {-1872.4781, 232.3500}, {-1917.3815, 210.9899}, {-1955.5608, 192.7801}, {-2037.0277, 153.8567},
{-2053.8111, 145.8379}, {-2077.7168, 132.9264}, {-2087.3488, 127.3010}, {-2097.1702, 121.5480}};
alignment::design base;
base.speed = 100.0;
base.start = {clicks.front().x, 0.0, clicks.front().y};
base.start_direction = glm::normalize(clicks[1] - clicks[0]);
base.end = {clicks.back().x, 0.0, clicks.back().y};
base.end_direction = glm::normalize(clicks.back() - clicks[clicks.size() - 2]);
std::vector<glm::dvec2> const points(clicks.begin() + 1, clicks.end());
auto const fit{alignment::fit_trace_stretch(base, points, 0, true)};
REQUIRE(fit.valid);
auto laid{base};
laid.vertices = fit.vertices;
auto const line{alignment::compute(laid)};
REQUIRE(line.valid);
// no arc flatter than R 3000 between the curves, the straight a straight
for (auto const &element : line.elements)
if (element.vertex >= 0 && element.radius > 0.0)
CHECK(element.radius < 3000.0);
CHECK(line.curves.size() == 2);
CHECK(fit.worst < 1.0);
}
TEST_CASE("alignment: curves turning the same way with straights between them, traced in the mode of a curve, two or three") {
double const k{1.0 / 300.0};
double const m{1.0 / 500.0};
struct shape {
char const *name;
std::vector<std::array<double, 3>> segments;
std::vector<double> radii;
double noise;
};
std::vector<shape> const shapes{
{"two, a straight of 150 m between", {{0, 0, 20}, {0, k, 40}, {k, k, 60}, {k, 0, 40}, {0, 0, 150}, {0, m, 40}, {m, m, 40}, {m, 0, 40}}, {300.0, 500.0}, 0.0},
{"the same clicked 5 cm off", {{0, 0, 20}, {0, k, 40}, {k, k, 60}, {k, 0, 40}, {0, 0, 150}, {0, m, 40}, {m, m, 40}, {m, 0, 40}}, {300.0, 500.0}, 0.05},
{"three, straights of 120 m between", {{0, 0, 20}, {0, k, 40}, {k, k, 40}, {k, 0, 40}, {0, 0, 120}, {0, m, 40}, {m, m, 60}, {m, 0, 40}, {0, 0, 120}, {0, k, 40}, {k, k, 40}, {k, 0, 40}},
{300.0, 500.0, 300.0}, 0.0},
};
for (auto const &each : shapes) {
std::string const name{each.name};
CAPTURE(name);
line_end end;
auto points{walked_points(each.segments, 10.0, end)};
for (std::size_t i = 0; i < points.size(); ++i)
points[i].y += each.noise * (i % 3 == 0 ? 1.0 : i % 3 == 1 ? -0.6 : 0.2);
alignment::design base;
base.speed = 60.0;
base.start = {0.0, 0.0, 0.0};
base.start_direction = heading(0.0);
base.end = {end.position.x, 0.0, end.position.y};
base.end_direction = heading(end.heading);
auto const fit{alignment::fit_trace_stretch(base, points, 0, true)};
REQUIRE(fit.valid);
REQUIRE(fit.vertices.size() == each.radii.size());
for (std::size_t i = 0; i < each.radii.size(); ++i) {
CHECK_FALSE(fit.vertices[i].compound);
CHECK(fit.vertices[i].radius == doctest::Approx(each.radii[i]).epsilon(0.03));
}
CHECK(fit.rms < 0.02 + each.noise);
}
}