From d2d06817bc36582cdcfef617e5d94ba2daec5fe9 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 13:20:47 +0200 Subject: [PATCH] feat(editor): the track laid on the terrain gets its grade line from the profile solver In place of a line bent every ten metres to keep within 0.1 m of every bump, the grades and the vertical curves are fitted to the ground the way the profile fits them. Where the formation is made in the terrain model the line keeps to the radius of the norms and cuts and fills; over the ground left as it is it bends as sharply as it has to. The joints keep the height and the grade of the track joined, no grade is capped and no piece is under 5 m. --- application/editormode.h | 2 + application/editormodenmt.cpp | 14 ++++ application/editormodetrackbed.cpp | 30 +++++++- editor/editorGroundFollow.cpp | 108 +++++++++++++++++------------ editor/editorGroundFollow.hpp | 14 +++- 5 files changed, 120 insertions(+), 48 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 7932570d..da6b1501 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -406,6 +406,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri void nmt_follow_tracks(); // the height the ground will have where the terrain is made ahead of the track laid and isn't there yet bool nmt_ahead(double const X, double const Z, double &Height) const; + // a track laid through the points gets its formation from the terrain of the model: it needn't keep to every bump of it + bool nmt_formed(std::vector const &Points) const; // the squares of the model under the rectangle are in, none of them waiting to be asked for again bool nmt_ready(glm::dvec2 const &Min, glm::dvec2 const &Max); // Only: the paths of these tracks are shaped anew, the modifiers of those removed taken away; null: all of them. the number diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index f1aa3c73..9fdcaa78 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -640,6 +640,20 @@ bool editor_mode::nmt_ahead(double const X, double const Z, double &Height) cons return true; } +bool editor_mode::nmt_formed(std::vector const &Points) const +{ + auto const &state{m_nmt_state}; + if (false == state.shape || false == m_nmt.placed() || false == m_streamer.active()) + return false; + bool foreign{false}; + if (nmt_terrain_offset(Points, foreign).has_value()) + return true; + if (foreign) + return false; + double height; + return std::all_of(Points.begin(), Points.end(), [&](glm::dvec2 const &Point) { return false == m_streamer.height_at(Point.x, Point.y, height) && nmt_ahead(Point.x, Point.y, height); }); +} + editor_mode::nmt_chunk editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key, std::optional const Offset) { auto const low{heightmap::chunk_corner(Key)}; diff --git a/application/editormodetrackbed.cpp b/application/editormodetrackbed.cpp index 92f1efc8..64abc0da 100644 --- a/application/editormodetrackbed.cpp +++ b/application/editormodetrackbed.cpp @@ -25,6 +25,11 @@ namespace using geometry::plan_distance; using geometry::plan_of; +double constexpr kSteepest{1000.0}; // per mille: the grades of the user's own track aren't capped +double constexpr kFormed{0.5}; // m, how closely the line keeps to the ground the formation is made in +double constexpr kBare{0.1}; // m, to the ground left as it is +double constexpr kBareRadius{300.0}; // m, the vertical curves over it + } // namespace glm::dvec3 editor_mode::cursor_terrain(double const Height) @@ -124,7 +129,30 @@ double editor_mode::bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direct void editor_mode::follow_ground(std::vector &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const { - ground_follow::follow(Pieces, {Height0, Grade0, Height1, Grade1}, [&](std::vector const &Points, std::vector const &Directions) { return bed_heights(Points, Directions, Style); }); + std::vector points; + for (auto const &piece : Pieces) + { + geometry::bezier const curve{piece}; + auto const count{std::max(6, static_cast(std::ceil(curve.plan_length() / 10.0)))}; + for (int i = 0; i <= count; ++i) + points.push_back(plan_of(curve.point(static_cast(i) / count))); + } + // the ground the formation is made in lets the line keep to the norms of the profile, cutting and filling; the ground left as + // it is the line follows, bending as sharply as it has to + auto const formed{nmt_formed(points)}; + ground_follow::grading grading; + grading.settings.norms = m_profile.line.norms; + grading.settings.norms.grade_max = kSteepest; + grading.settings.norms.curve_threshold = 0.0; + if (false == formed) + { + grading.settings.norms.radius_factor = 0.0; + grading.settings.norms.radius_min = kBareRadius; + } + grading.settings.speed = design_speed(Style, nullptr); + grading.fit.grade_max = kSteepest; + grading.fit.tolerance = formed ? kFormed : kBare; + ground_follow::follow(Pieces, {Height0, Grade0, Height1, Grade1}, [&](std::vector const &Points, std::vector const &Directions) { return bed_heights(Points, Directions, Style); }, grading); } glm::dvec3 editor_mode::lay_bed(glm::dvec3 const &Point) diff --git a/editor/editorGroundFollow.cpp b/editor/editorGroundFollow.cpp index cdd24a30..d30030a8 100644 --- a/editor/editorGroundFollow.cpp +++ b/editor/editorGroundFollow.cpp @@ -13,7 +13,9 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include +#include namespace ground_follow { @@ -27,8 +29,7 @@ using geometry::part_of; using geometry::plan_of; double constexpr kStep{2.0}; -double constexpr kTolerance{0.1}; -double constexpr kSpacing{10.0}; +double constexpr kShortest{5.0}; // m, the shortest piece the line cuts off struct samples { @@ -61,27 +62,39 @@ samples sample(std::vector const &Pieces, std::vector cons return result; } -void fit(knot_line &Line, std::vector const &Chainages, std::vector const &Targets) +bool grade_line(samples const &Points, std::vector const &Targets, double const Total, ends const &Ends, grading const &Grading, profile::line &Line) { - for (std::size_t pass = 0; pass < Chainages.size(); ++pass) - { - auto worst{kTolerance}; - auto pick{Chainages.size()}; - for (std::size_t j = 0; j < Chainages.size(); ++j) - { - if (std::isnan(Targets[j])) - continue; - double grade{0.0}; - auto const miss{std::abs(Line.at(Chainages[j], grade) - Targets[j])}; - if (miss <= worst || false == Line.room(Chainages[j], kSpacing)) - continue; - worst = miss; - pick = j; - } - if (pick == Chainages.size()) - return; - Line.insert(Chainages[pick], Targets[pick]); - } + auto const none{std::numeric_limits::quiet_NaN()}; + std::vector chainages{0.0}; + std::vector ground{none}; + chainages.insert(chainages.end(), Points.chainages.begin(), Points.chainages.end()); + ground.insert(ground.end(), Targets.begin(), Targets.end()); + chainages.push_back(Total); + ground.push_back(none); + if (std::none_of(ground.begin(), ground.end(), [](double const Height) { return std::isfinite(Height); })) + return false; + profile::context context; + context.length = Total; + context.start_elevation = Ends.height0; + context.end_elevation = Ends.height1; + context.start_joined = false == std::isnan(Ends.grade0); + context.end_joined = false == std::isnan(Ends.grade1); + context.start_grade = context.start_joined ? Ends.grade0 : 0.0; + context.end_grade = context.end_joined ? Ends.grade1 : 0.0; + // the start of the track stays where it's laid from, a free end goes where the line takes it + auto fit{Grading.fit}; + if (false == context.start_joined) + fit.bounds.push_back({0.0, Ends.height0, Ends.height0}); + auto settings{Grading.settings}; + settings.join_start = true; + settings.join_end = true; + Line = profile::fit_ground(chainages, ground, context, settings, fit, {}, std::vector(chainages.size(), Grading.reach)); + if (Line.points.size() < 2) + return false; + profile::complete(Line, context); + if (false == context.start_joined) + Line.points.front().elevation = Ends.height0; + return true; } std::vector cut(std::vector const &Pieces, std::vector const &Lengths, std::vector const &Knots, std::vector> &Spans) @@ -96,7 +109,7 @@ std::vector cut(std::vector const &Pieces, std::vect double parameter{0.0}; for (auto const knot : Knots) { - if (knot < cut + 0.5 || knot > to - 0.5) + if (knot < cut + kShortest || knot > to - kShortest) continue; auto const next{curve.parameter(knot - from)}; result.push_back(part_of(Pieces[i], parameter, next)); @@ -111,7 +124,7 @@ std::vector cut(std::vector const &Pieces, std::vect return result; } -void lift(segment_data &Piece, std::pair const &Span, knot_line const &Line) +void lift(segment_data &Piece, std::pair const &Span, std::function const &At) { auto &points{Piece.points}; auto const [a, b]{Span}; @@ -122,8 +135,8 @@ void lift(segment_data &Piece, std::pair const &Span, knot_line auto &finish{points[segment_data::point::end]}; auto &control1{points[segment_data::point::control1]}; auto &control2{points[segment_data::point::control2]}; - start.y = Line.at(a, begin); - finish.y = Line.at(b, end); + start.y = At(a, begin); + finish.y = At(b, end); if (has_controls(Piece)) { control1.y = begin * glm::length(plan_of(control1)); @@ -156,14 +169,6 @@ void knot_line::refresh() slopes.back() = grade1; } -void knot_line::insert(double const Chainage, double const Height) -{ - auto const where{std::upper_bound(knots.begin(), knots.end(), Chainage) - knots.begin()}; - knots.insert(knots.begin() + where, Chainage); - heights.insert(heights.begin() + where, Height); - refresh(); -} - double knot_line::at(double const Chainage, double &Grade) const { auto const next{static_cast(std::upper_bound(knots.begin(), knots.end(), Chainage) - knots.begin())}; @@ -172,13 +177,7 @@ double knot_line::at(double const Chainage, double &Grade) const return geometry::hermite(length, heights[i], slopes[i], heights[i + 1], slopes[i + 1], std::clamp((Chainage - knots[i]) / length, 0.0, 1.0), Grade); } -bool knot_line::room(double const Chainage, double const Spacing) const -{ - auto const next{std::upper_bound(knots.begin(), knots.end(), Chainage)}; - return next != knots.end() && *next - Chainage >= Spacing && Chainage - *std::prev(next) >= Spacing; -} - -void follow(std::vector &Pieces, ends const &Ends, bed_query const &Bed) +void follow(std::vector &Pieces, ends const &Ends, bed_query const &Bed, grading const &Grading) { std::vector lengths; double total{0.0}; @@ -188,13 +187,34 @@ void follow(std::vector &Pieces, ends const &Ends, bed_query const return; auto const points{sample(Pieces, lengths, total, Ends)}; auto const targets{Bed(points.points, points.directions)}; + std::vector> spans; + if (profile::line graded; grade_line(points, targets, total, Ends, Grading, graded)) + { + // the ends of the track go on in the height and the grade of the tracks they join, whatever the line found + auto result{cut(Pieces, lengths, profile::breaks(graded), spans)}; + for (std::size_t i = 0; i < result.size(); ++i) + lift(result[i], spans[i], [&](double const Chainage, double &Grade) { + if (Chainage < 1e-6 && false == std::isnan(Ends.grade0)) + { + Grade = Ends.grade0; + return Ends.height0; + } + if (Chainage > total - 1e-6 && false == std::isnan(Ends.grade1)) + { + Grade = Ends.grade1; + return Ends.height1; + } + Grade = profile::grade(graded, Chainage); + return profile::elevation(graded, Chainage); + }); + Pieces = std::move(result); + return; + } knot_line line{{0.0, total}, {Ends.height0, Ends.height1}, {}, Ends.grade0, Ends.grade1}; line.refresh(); - fit(line, points.chainages, targets); - std::vector> spans; auto result{cut(Pieces, lengths, line.knots, spans)}; for (std::size_t i = 0; i < result.size(); ++i) - lift(result[i], spans[i], line); + lift(result[i], spans[i], [&](double const Chainage, double &Grade) { return line.at(Chainage, Grade); }); Pieces = std::move(result); } diff --git a/editor/editorGroundFollow.hpp b/editor/editorGroundFollow.hpp index 9b532b35..cb843996 100644 --- a/editor/editorGroundFollow.hpp +++ b/editor/editorGroundFollow.hpp @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include "editor/editorProfile.hpp" #include "world/Segment.h" namespace ground_follow @@ -37,11 +38,18 @@ struct knot_line double grade1{0.0}; void refresh(); - void insert(double const Chainage, double const Height); double at(double const Chainage, double &Grade) const; - bool room(double const Chainage, double const Spacing) const; }; -void follow(std::vector &Pieces, ends const &Ends, bed_query const &Bed); +// the grade line the track is laid on where there's ground under it: the solver of the grade lines fits it to the ground +struct grading +{ + profile::line settings; // the speed and the norms of the line + profile::ground_fit fit; // how closely it keeps to the ground + double reach{100.0}; // m, how far the ground may part from the line and still be followed +}; + +// where Bed finds no ground, the heights lead from one end to the other +void follow(std::vector &Pieces, ends const &Ends, bed_query const &Bed, grading const &Grading = {}); } // namespace ground_follow