From ed3d6551d8122754f689783607cea096e98242ec Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 26 Oct 2026 19:50:00 +0100 Subject: [PATCH] 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 --- editor/editorAlignment.cpp | 119 ++++++++++++++++++++++------------ tests/unit/test_alignment.cpp | 67 +++++++++++++++++++ 2 files changed, 144 insertions(+), 42 deletions(-) diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 717bc43f..3844b618 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -3857,6 +3857,25 @@ void fit_compound(design &Design, std::size_t const Vertex, std::vector 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 fit_arcs(design const &Single, std::vector= static_cast(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 fit_chain(design const &Base, std::vector 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 fit_chain(design const &Base, std::vector const &Points, double const Speed, bool const Straights) { auto const n{Points.size()}; if (n < 6) @@ -4041,7 +4091,7 @@ std::optional fit_chain(design const &Base, std::vector 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 fit_chain(design const &Base, std::vector= static_cast(count)) - return std::optional{}; - 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{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 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 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 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 const &Parameters, design &Trial) { @@ -4364,8 +4392,8 @@ trace_stretch fit_trace_stretch(design const &Base, std::vector 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 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; } diff --git a/tests/unit/test_alignment.cpp b/tests/unit/test_alignment.cpp index 79acffe7..54e1cfe0 100644 --- a/tests/unit/test_alignment.cpp +++ b/tests/unit/test_alignment.cpp @@ -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 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 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> segments; + std::vector radii; + double noise; + }; + std::vector 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); + } +}