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); + } +}