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