diff --git a/application/editormode.h b/application/editormode.h index b85704eb..72df0eef 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -436,8 +436,6 @@ 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 @@ -466,8 +464,18 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::size_t nmt_join_ends(); void nmt_auto_start(); void nmt_auto_step(); - // the grade line of the line of tracks through the track fitted to the terrain model and applied; false if it couldn't be - bool nmt_fit_line(TTrack &Track); + // the grade line of the line of tracks through the track fitted to the terrain model and applied; false if it couldn't be. + // The line runs over the tracks not Fitted, the ones fitted stay as they are and the line meets them. Where the model falls + // away under the line by the abutments the tracks go over on a bridge; Laid: the tracks just laid, the formation fills it and + // they are cut only where they can't follow the line otherwise + bool nmt_fit_line(TTrack &Track, std::function const &Fitted, bool const Laid); + // the tracks laid in one go since the history step From put onto the terrain model as one line, as the tracks go onto the + // terrain: the track ending at End then the one the laying goes on from + bool nmt_fit_laid(std::size_t const From, glm::dvec3 const End); + // the tracks laid one after another on one straight or one arc, cut by the grade lines fitted before, made one again + void nmt_merge_laid(std::unordered_set &Laid); + // the points of the paths of the track about a metre apart, in squares of the plan + static void nmt_sample_paths(TTrack const &Track, std::map, std::vector> &Points); // the tool of the terrain the left mouse button works enum class terrain_tool { @@ -1384,6 +1392,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri glm::dvec3 point{0.0}; }; std::vector chain; + std::size_t history{0}; // the step of the history the chain was begun at }; lay_tool m_lay; bool m_norms_free{false}; @@ -2335,7 +2344,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void profile_check(); // Terrain: the heightmap terrain follows the tracks changed. Bridges: stretches of the route over a bridge or a viaduct, the // paths over them cut off at their ends and set to the bridge environment - void profile_apply(bool const Terrain = false, std::vector const &Bridges = {}); + void profile_apply(bool const Terrain = false, std::vector const &Bridges = {}, double const Fewest = 0.0); // the terrain under the tracks changed by the last Edits steps of the history which changed tracks void profile_follow_terrain(std::size_t const Edits); void profile_fit_view(); diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index dc173cdd..9e4e6bcb 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -66,7 +66,7 @@ void editor_mode::update_build_tools() m_extend.mouse = cursor_terrain(m_extend.point.y); m_extend.snap = m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; if (m_extend.snap.track == nullptr) - m_extend.mouse.y = bed_height(m_extend.mouse, plan_of(m_extend.mouse - m_extend.point), m_extend.track); + m_extend.mouse.y = EditorSettings.heights() == editorSettings::lay_heights::level ? m_extend.point.y : bed_height(m_extend.mouse, plan_of(m_extend.mouse - m_extend.point), m_extend.track); joining(m_extend.snap); } if (m_switch.placing) @@ -634,12 +634,14 @@ void editor_mode::finish_extend() give_own_speed(created); editor_track::commit(created); digitize_drawn(created); + auto const from{m_history.size()}; push_track_snapshot({}, created); first_step_done(first_step::laid); if (target.track != nullptr) first_step_done(first_step::join); m_node = created.back(); ui()->set_node(m_node); + nmt_fit_laid(from, created.back()->m_paths.front().points[segment_data::point::end]); straight_refresh(); } @@ -1122,6 +1124,8 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) give_own_speed(created); editor_track::commit(created); digitize_drawn(created); + if (tool.chain.empty()) + tool.history = m_history.size(); push_track_snapshot({}, created); first_step_done(first_step::laid); if (End.track != nullptr) @@ -1152,11 +1156,13 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) } if (scene::Layers.active() == null_handle && style == nullptr) tool.status += "\n" + STR("The scenery isn't opened for editing: new track won't be saved"); + m_node = created.back(); + ui()->set_node(m_node); + if (nmt_fit_laid(tool.history, created.back()->m_paths.front().points[segment_data::point::end])) + tool.status += "\n" + STR("The grade line of the whole track laid in a row fitted to the terrain model, as the tracks go onto the terrain"); WriteLog("Editor: " + tool.status, logtype::generic); tool.points.clear(); tool.start = {}; - m_node = created.back(); - ui()->set_node(m_node); straight_refresh(); } @@ -1522,6 +1528,16 @@ void editor_mode::render_lay_ui() : "Off: a track laid on from another one, or joined to it, is designed for the speed of that track and takes it.\nOn: it's designed for the speed above and the new tracks get it.\nWhile laying: Ctrl+Shift+wheel"); if (auto const [from, to]{laying_ends()}; false == m_norms_free && (from != nullptr || to != nullptr)) ImGui::TextDisabled("%s", design_speed_of(from, to).c_str()); + ImGui::TextDisabled("%s", STR_C("Height of the track on the ground")); + if (int heights{static_cast(EditorSettings.heights())}; + segmented("layheights", heights, + {{0, STR_C("Fitted"), "The grades fitted to the ground as Tracks onto the terrain fits them: vertical curves of the norms, the bumps of the ground gone past, the earthworks made by the terrain. On the terrain model the whole track laid in a row is fitted again at each click"}, + {1, STR_C("Point to point"), "Each point clicked at the height of the ground, an even grade between them: the grade changes only where you click"}, + {2, STR_C("Flat"), "The track goes on at the height it's laid from, whatever the ground; the first point on the ground"}})) + { + EditorSettings.heights(static_cast(heights)); + EditorSettings.save(); + } if (auto *track{selected_track()}; track != nullptr) { std::string reason; diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index 7760a619..e13e5c2e 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -751,20 +751,6 @@ 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)}; @@ -1675,11 +1661,10 @@ void editor_mode::nmt_auto_start() state.status = STR("Fetching the terrain model under the tracks..."); } -bool editor_mode::nmt_fit_line(TTrack &Track) +bool editor_mode::nmt_fit_line(TTrack &Track, std::function const &Fitted, bool const Laid) { auto &profile{m_profile}; - auto const &done{m_nmt_state.done}; - auto const fitted = [&](TTrack const *Other) { return std::find(done.begin(), done.end(), Other) != done.end(); }; + auto const &fitted{Fitted}; // the line runs over the tracks not fitted yet around the track: the ones fitted stay as they are auto route{editor_track::run_route(Track, profile.run_length)}; auto const at{std::find_if(route.spans.begin(), route.spans.end(), [&](auto const &Span) { return Span.track == &Track; })}; @@ -2076,11 +2061,12 @@ bool editor_mode::nmt_fit_line(TTrack &Track) // spans a few metres apart are one bridge on its piers; what is shorter than that is a culvert under the embankment double constexpr kPier{5.0}; // m double constexpr kCulvert{3.0}; // m + double constexpr kFewest{0.03}; // m, how far a path just laid may part from the line before it's cut std::vector heights(count); for (std::size_t i = 0; i < count; ++i) heights[i] = profile::elevation(fitted_line, sampled[i]); std::vector bridges; - for (auto const &stretch : profile::bridged(sampled, heights, model, kBridged, kAbutment)) + for (auto const &stretch : Laid ? std::vector{} : profile::bridged(sampled, heights, model, kBridged, kAbutment)) if (false == bridges.empty() && stretch.from - bridges.back().to < kPier) { bridges.back().to = stretch.to; @@ -2089,7 +2075,7 @@ bool editor_mode::nmt_fit_line(TTrack &Track) else bridges.push_back(stretch); bridges.erase(std::remove_if(bridges.begin(), bridges.end(), [&](profile::departure const &Bridge) { return Bridge.to - Bridge.from < kCulvert; }), bridges.end()); - profile_apply(false, bridges); + profile_apply(false, bridges, Laid ? kFewest : 0.0); auto const applied{profile.error.empty()}; // where the line parts from the model: under it a gap, a bridge or a viaduct, over it a wall or a structure above the track auto const parted{profile::departures(fitted_line, sampled, model, 2.0 * kModel)}; @@ -2186,6 +2172,200 @@ bool editor_mode::nmt_fit_line(TTrack &Track) return applied; } +void editor_mode::nmt_sample_paths(TTrack const &Track, std::map, std::vector> &Points) +{ + for (auto const &path : Track.m_paths) + { + geometry::bezier const curve{path}; + auto const chord{glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end])}; + auto const count{std::max(1, static_cast(std::ceil(chord * 1.1)))}; + for (int i = 0; i <= count; ++i) + { + auto const t{static_cast(i) / count}; + auto const at{curve.point(t)}; + auto const tangent{curve.first(t)}; + auto const run{glm::length(glm::dvec2{tangent.x, tangent.z})}; + if (run > 1e-9) + Points[{static_cast(std::floor(at.x / kPlanCell)), static_cast(std::floor(at.z / kPlanCell))}].push_back( + {at, glm::dvec2{tangent.x, tangent.z} / run, tangent.y / run, at.y, static_cast(Track.rail_height()), &Track}); + } + } +} + +void editor_mode::nmt_merge_laid(std::unordered_set &Laid) +{ + auto const direction = [](segment_data const &Path) { + auto const control{geometry::plan_of(Path.points[segment_data::point::control1])}; + auto const chord{geometry::plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])}; + auto const along{glm::length(control) > 1e-6 ? control : chord}; + return along / std::max(1e-12, glm::length(along)); + }; + auto const straight = [&](segment_data const &Path) { + if (std::abs(Path.radius) > 1e-3f) + return false; + auto const along{direction(Path)}; + return std::abs(geometry::cross(along, geometry::plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start]))) < 1e-3 + && std::abs(geometry::cross(along, geometry::plan_of(Path.points[segment_data::point::control2]))) < 1e-3; + }; + struct circle + { + glm::dvec2 centre{0.0}; + double radius{0.0}; + int side{0}; + double angle{0.0}; + }; + auto const arc = [&](segment_data const &Path) -> std::optional { + auto const radius{std::abs(static_cast(Path.radius))}; + if (radius < 1e-3 || glm::length(geometry::plan_of(Path.points[segment_data::point::control1])) < 1e-6) + return std::nullopt; + auto const start{geometry::plan_of(Path.points[segment_data::point::start])}; + auto const end{geometry::plan_of(Path.points[segment_data::point::end])}; + auto const along{direction(Path)}; + auto const side{geometry::cross(along, end - start) > 0.0 ? 1 : -1}; + circle result{start + glm::dvec2{-along.y, along.x} * (side * radius), radius, side, 0.0}; + if (std::abs(glm::distance(result.centre, end) - radius) > 0.01) + return std::nullopt; + result.angle = std::abs(geometry::signed_angle(start - result.centre, end - result.centre)); + return result; + }; + auto const alike = [](TTrack const &A, TTrack const &B) { + return editor_track::velocity(A) == editor_track::velocity(B) && A.eEnvironment == B.eEnvironment && A.iCategoryFlag == B.iCategoryFlag && A.iDamageFlag == B.iDamageFlag; + }; + std::vector> states; + std::vector removed; + for (bool merged{true}; merged;) + { + merged = false; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || false == Laid.contains(track) || track->eType != tt_Normal || track->m_paths.size() != 1) + continue; + auto *next{track->trNext}; + if (next == nullptr || next == track || next->m_editorremoved || false == Laid.contains(next) || next->eType != tt_Normal || next->m_paths.size() != 1 || next->trPrev != track || false == alike(*track, *next)) + continue; + auto const &first{track->m_paths.front()}; + auto const &second{next->m_paths.front()}; + if (glm::distance(first.points[segment_data::point::end], second.points[segment_data::point::start]) > 1e-3) + continue; + auto const length{geometry::bezier(first).plan_length() + geometry::bezier(second).plan_length()}; + if (length > geometry::longest_path) + continue; + auto path{first}; + path.pieces.clear(); + if (straight(first) && straight(second) && std::abs(geometry::cross(direction(first), direction(second))) < 1e-5 && glm::dot(direction(first), direction(second)) > 0.0) + { + path.points[segment_data::point::control1] = {}; + path.points[segment_data::point::control2] = {}; + path.radius = 0.f; + } + else if (auto const a{arc(first)}, b{arc(second)}; + a && b && a->side == b->side && std::abs(a->radius - b->radius) < 1e-3 * a->radius && glm::distance(a->centre, b->centre) < 0.01 && a->angle + b->angle <= glm::radians(90.0)) + { + auto const pieces{geometry::arc_pieces(first.points[segment_data::point::start], direction(first), 0.0, a->radius, a->angle + b->angle, a->side)}; + if (pieces.size() != 1) + continue; + path.points[segment_data::point::control1] = pieces.front().points[segment_data::point::control1]; + path.points[segment_data::point::control2] = pieces.front().points[segment_data::point::control2]; + } + else + continue; + path.points[segment_data::point::end] = second.points[segment_data::point::end]; + path.rolls[1] = second.rolls[1]; + if (std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == track; })) + states.emplace_back(track, editor_track::capture(*track)); + editor_track::retire(*next); + Laid.erase(next); + removed.push_back(next); + track->m_paths.front() = path; + editor_track::commit({track}); + merged = true; + } + } + if (false == removed.empty()) + push_track_snapshot(std::move(states), {}, std::move(removed)); +} + +bool editor_mode::nmt_fit_laid(std::size_t const From, glm::dvec3 const End) +{ + auto &state{m_nmt_state}; + if (EditorSettings.heights() != editorSettings::lay_heights::fitted || state.stage != nmt_state::automatic::idle || false == m_nmt.placed() || From >= m_history.size()) + return false; + std::unordered_set laid; + for (auto step{From}; step < m_history.size(); ++step) + for (auto const *track : m_history[step].created) + if (track != nullptr && false == track->m_editorremoved && rail(*track)) + laid.insert(track); + auto const at_end = [&]() -> TTrack * { + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && laid.contains(track) && track->eType == tt_Normal && geometry::plan_distance(track->m_paths.front().points[segment_data::point::end], End) < 0.01) + return track; + return nullptr; + }; + auto const steps{m_history.size()}; + nmt_merge_laid(laid); + auto *last{at_end()}; + if (last == nullptr) + return false; + // the tracks around stay as they are, as lines fitted already: the line keeps level with the ones beside it, the more closely + // the nearer they are, and crosses the ones it crosses at their height + double constexpr kAround{60.0}; // m + glm::dvec2 low{std::numeric_limits::max()}; + glm::dvec2 high{std::numeric_limits::lowest()}; + for (auto const *track : laid) + for (auto const &path : track->m_paths) + for (auto const end : {segment_data::point::start, segment_data::point::end}) + { + low = glm::min(low, geometry::plan_of(path.points[end])); + high = glm::max(high, geometry::plan_of(path.points[end])); + } + low -= kAround; + high += kAround; + std::map, std::vector> around; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || false == rail(*track) || laid.contains(track)) + continue; + auto const &points{track->m_paths.front().points}; + auto const a{geometry::plan_of(points[segment_data::point::start])}; + auto const b{geometry::plan_of(points[segment_data::point::end])}; + if (std::max(a.x, b.x) < low.x || std::min(a.x, b.x) > high.x || std::max(a.y, b.y) < low.y || std::min(a.y, b.y) > high.y) + continue; + nmt_sample_paths(*track, around); + } + // the state of the tracks going onto the terrain lent to the one line, the profile tool left as it was + auto const profile{m_profile}; + auto fitted_lines{std::exchange(state.laid, around)}; + auto paths{std::exchange(state.paths, std::move(around))}; + auto loose{std::exchange(state.loose, {})}; + auto const departures{state.departures}; + auto const applied{nmt_fit_line(*last, [&](TTrack const *Other) { return false == laid.contains(Other); }, true)}; + auto const error{m_profile.error}; + state.laid = std::move(fitted_lines); + state.paths = std::move(paths); + state.loose = std::move(loose); + state.departures = departures; + m_profile = profile; + // taken back with the laying + for (auto step{steps}; step < m_history.size(); ++step) + m_history[step].joined = true; + if (applied) + { + laid.clear(); + for (auto step{From}; step < m_history.size(); ++step) + for (auto const *track : m_history[step].created) + if (track != nullptr && false == track->m_editorremoved) + laid.insert(track); + if (auto *track{at_end()}; track != nullptr) + { + m_node = track; + ui()->set_node(m_node); + } + } + else if (false == error.empty()) + WriteLog("Editor: the track laid not put onto the terrain model - " + error, logtype::generic); + return applied; +} + void editor_mode::nmt_auto_step() { auto &state{m_nmt_state}; @@ -2247,22 +2427,7 @@ void editor_mode::nmt_auto_step() state.paths.clear(); for (auto *track : simulation::Paths.sequence()) if (track != nullptr && rail(*track)) - for (auto const &path : track->m_paths) - { - geometry::bezier const curve{path}; - auto const chord{glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end])}; - auto const count{std::max(1, static_cast(std::ceil(chord * 1.1)))}; - for (int i = 0; i <= count; ++i) - { - auto const t{static_cast(i) / count}; - auto const at{curve.point(t)}; - auto const tangent{curve.first(t)}; - auto const run{glm::length(glm::dvec2{tangent.x, tangent.z})}; - if (run > 1e-9) - state.paths[{static_cast(std::floor(at.x / kPlanCell)), static_cast(std::floor(at.z / kPlanCell))}].push_back( - {at, glm::dvec2{tangent.x, tangent.z} / run, tangent.y / run, at.y, static_cast(track->rail_height()), track}); - } - } + nmt_sample_paths(*track, state.paths); state.stage = nmt_state::automatic::fitting; return; } @@ -2289,7 +2454,7 @@ void editor_mode::nmt_auto_step() auto *track{state.left.front()}; auto const name{track->name()}; auto const known{simulation::Paths.sequence().size()}; - if (nmt_fit_line(*track)) + if (nmt_fit_line(*track, [&](TTrack const *Other) { return fitted.contains(Other); }, false)) ++state.lines; else ++state.refused; diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 0d0add80..31835241 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -1450,7 +1450,7 @@ void editor_mode::profile_frame_view() fly_to(centre - right * (shift.x * pixel) + above * (shift.y * pixel), look, distance, 0.35); } -void editor_mode::profile_apply(bool const Terrain, std::vector const &Bridges) +void editor_mode::profile_apply(bool const Terrain, std::vector const &Bridges, double const Fewest) { auto &state{m_profile}; state.status.clear(); @@ -1475,12 +1475,14 @@ void editor_mode::profile_apply(bool const Terrain, std::vector> states; std::vector created; - auto cuts{profile::breaks(line)}; + std::vector ends; + for (auto const &span : state.route.spans) + ends.insert(ends.end(), {span.from, span.to}); + auto cuts{Fewest > 0.0 ? profile::fewest_breaks(line, ends, Fewest) : profile::breaks(line)}; // a bridge ends at a joint or a break of the line by its abutment rather than leave a sliver of a path there double constexpr kBridgeSnap{1.0}; // m auto joints{cuts}; - for (auto const &span : state.route.spans) - joints.insert(joints.end(), {span.from, span.to}); + joints.insert(joints.end(), ends.begin(), ends.end()); auto bridges{Bridges}; for (auto &bridge : bridges) for (auto *end : {&bridge.from, &bridge.to}) diff --git a/application/editormodeselftestjoin.cpp b/application/editormodeselftestjoin.cpp index 1695da02..93b79898 100644 --- a/application/editormodeselftestjoin.cpp +++ b/application/editormodeselftestjoin.cpp @@ -129,6 +129,14 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma WriteLog(format("SELFTEST %s end: %.3f m from the target", command.c_str(), geometry::plan_distance(previous, b))); return selftest_result::handled; } + if (command == "layheights") + { + std::string which; + words >> which; + EditorSettings.heights(which == "points" ? editorSettings::lay_heights::points : which == "level" ? editorSettings::lay_heights::level : editorSettings::lay_heights::fitted); + WriteLog("SELFTEST layheights: " + which); + return selftest_result::handled; + } if (command == "norms") { std::string which; @@ -237,34 +245,47 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma double hanging{0.0}; double buried{0.0}; int paths{0}; + // the paths laid as they lie now, along the line: a grade line fitted afterwards cut some of them + std::vector laid; for (auto step{before}; step < m_history.size(); ++step) for (auto const *track : m_history[step].created) + if (track != nullptr && false == track->m_editorremoved && std::find(laid.begin(), laid.end(), track) == laid.end()) + laid.push_back(track); + if (false == laid.empty()) + { + auto const origin{laid.front()->m_paths.front().points[segment_data::point::start]}; + auto const along{glm::normalize(geometry::plan_of(laid.back()->m_paths.front().points[segment_data::point::end] - origin) + glm::dvec2{1e-9, 0.0})}; + std::stable_sort(laid.begin(), laid.end(), [&](TTrack const *A, TTrack const *B) { + return glm::dot(geometry::plan_of(A->m_paths.front().points[segment_data::point::start] - origin), along) < glm::dot(geometry::plan_of(B->m_paths.front().points[segment_data::point::start] - origin), along); + }); + } + for (auto const *track : laid) + { + auto const &path{track->m_paths.front()}; + geometry::bezier const curve{path}; + auto const begin{glm::normalize(geometry::plan_of(curve.first(0.0)))}; + auto const &a{path.points[segment_data::point::start]}; + auto const &b{path.points[segment_data::point::end]}; + auto const rise = [](glm::dvec3 const &Tangent) { return Tangent.y / std::max(1e-9, glm::length(geometry::plan_of(Tangent))); }; + WriteLog(format("SELFTEST layclicks path %s: %.2f %.2f -> %.2f %.2f, L %.2f m, R %.1f m, kink %.4f deg, step %.4f m, y %.3f -> %.3f, grade %.2f -> %.2f permille, grade break %.3f permille", track->name().c_str(), a.x, a.z, b.x, b.z, curve.plan_length(), static_cast(path.radius), + first ? 0.0 : glm::degrees(std::abs(geometry::signed_angle(heading, begin))), first ? 0.0 : glm::distance(previous, a), a.y, b.y, 1000.0 * rise(curve.first(0.0)), 1000.0 * rise(curve.first(1.0)), + first ? 0.0 : 1000.0 * std::abs(rise(curve.first(0.0)) - slope))); + slope = rise(curve.first(1.0)); + heading = glm::normalize(geometry::plan_of(curve.first(1.0))); + previous = b; + first = false; + auto const count{std::max(1, static_cast(std::ceil(curve.plan_length() / 2.0)))}; + for (int k = 0; k <= count; ++k) { - auto const &path{track->m_paths.front()}; - geometry::bezier const curve{path}; - auto const begin{glm::normalize(geometry::plan_of(curve.first(0.0)))}; - auto const &a{path.points[segment_data::point::start]}; - auto const &b{path.points[segment_data::point::end]}; - auto const rise = [](glm::dvec3 const &Tangent) { return Tangent.y / std::max(1e-9, glm::length(geometry::plan_of(Tangent))); }; - WriteLog(format("SELFTEST layclicks path %s: %.2f %.2f -> %.2f %.2f, L %.2f m, R %.1f m, kink %.4f deg, step %.4f m, y %.3f -> %.3f, grade %.2f -> %.2f permille, grade break %.3f permille", track->name().c_str(), a.x, a.z, b.x, b.z, curve.plan_length(), static_cast(path.radius), - first ? 0.0 : glm::degrees(std::abs(geometry::signed_angle(heading, begin))), first ? 0.0 : glm::distance(previous, a), a.y, b.y, 1000.0 * rise(curve.first(0.0)), 1000.0 * rise(curve.first(1.0)), - first ? 0.0 : 1000.0 * std::abs(rise(curve.first(0.0)) - slope))); - slope = rise(curve.first(1.0)); - heading = glm::normalize(geometry::plan_of(curve.first(1.0))); - previous = b; - first = false; - auto const count{std::max(1, static_cast(std::ceil(curve.plan_length() / 2.0)))}; - for (int k = 0; k <= count; ++k) - { - auto const t{curve.parameter(curve.plan_length() * k / count)}; - auto const target{bed_heights({curve.point(t) + glm::dvec3{0.0, 50.0, 0.0}}, {geometry::plan_of(curve.first(t))}, track).front()}; - if (std::isnan(target)) - continue; - hanging = std::max(hanging, curve.point(t).y - target); - buried = std::max(buried, target - curve.point(t).y); - } - ++paths; + auto const t{curve.parameter(curve.plan_length() * k / count)}; + auto const target{bed_heights({curve.point(t) + glm::dvec3{0.0, 50.0, 0.0}}, {geometry::plan_of(curve.first(t))}, track).front()}; + if (std::isnan(target)) + continue; + hanging = std::max(hanging, curve.point(t).y - target); + buried = std::max(buried, target - curve.point(t).y); } + ++paths; + } WriteLog(format("SELFTEST layclicks %d paths, ballast foot over the ground up to %.3f m, under it up to %.3f m", paths, hanging, buried)); return selftest_result::handled; } diff --git a/application/editormodetrackbed.cpp b/application/editormodetrackbed.cpp index 64abc0da..a51dc07c 100644 --- a/application/editormodetrackbed.cpp +++ b/application/editormodetrackbed.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormode.h" #include "editor/editorGeometry.hpp" #include "editor/editorGroundFollow.hpp" +#include "editor/editorSettings.hpp" #include "utilities/Globals.h" #include "world/Track.h" @@ -26,9 +27,7 @@ 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 +double constexpr kModel{0.15}; // m, how closely the line keeps to the ground, as to the terrain model } // namespace @@ -129,35 +128,35 @@ 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 { - std::vector points; - for (auto const &piece : Pieces) + // from point to point or level the heights lead from one end to the other, whatever the ground between + if (EditorSettings.heights() != editorSettings::lay_heights::fitted) { - 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))); + ground_follow::follow(Pieces, {Height0, Grade0, Height1, Grade1}, [](std::vector const &Points, std::vector const &) { return std::vector(Points.size(), std::numeric_limits::quiet_NaN()); }); + return; } - // 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)}; + // the line goes over the ground as the tracks go onto the terrain: the vertical curves of the norms, the grades as steep as the + // ground has them, a rise or a hollow longer than a structure followed closely, the bumps and what parts from it gone past 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; + grading.fit.tolerance = kModel; + grading.fit.follow_length = 60.0; + grading.fit.follow_height = 4.0; + grading.fit.follow_steepest = 100.0; + grading.fit.follow_tolerance = 0.05; + grading.reach = kModel; 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) { auto const &tool{m_lay}; + // level: on at the height the laying goes on from, the first point on the ground + if (EditorSettings.heights() == editorSettings::lay_heights::level && false == tool.points.empty()) + return {Point.x, tool.points.back().y, Point.z}; TTrack const *style{tool.start.track != nullptr ? tool.start.track : tool.copy_selected ? selected_track() : nullptr}; return {Point.x, bed_height(Point, tool.points.empty() ? glm::dvec2{0.0} : plan_of(Point - tool.points.back()), style), Point.z}; } diff --git a/editor/editorProfile.cpp b/editor/editorProfile.cpp index 9bbabf9b..4fbff5be 100644 --- a/editor/editorProfile.cpp +++ b/editor/editorProfile.cpp @@ -1203,6 +1203,55 @@ std::vector breaks(line const &Line) return result; } +std::vector fewest_breaks(line const &Line, std::vector Joints, double const Tolerance) +{ + std::sort(Joints.begin(), Joints.end()); + Joints.erase(std::unique(Joints.begin(), Joints.end(), [](double const A, double const B) { return std::abs(A - B) < 1e-6; }), Joints.end()); + auto const all{breaks(Line)}; + auto const inward = [&](double const Chainage, double const Other) { return grade(Line, Chainage + std::min(1e-3, std::abs(Other - Chainage) * 0.5) * (Other > Chainage ? 1.0 : -1.0)); }; + auto const follows = [&](double const From, double const To) { + auto const length{To - From}; + auto const h0{elevation(Line, From)}; + auto const h1{elevation(Line, To)}; + auto const g0{inward(From, To) * length}; + auto const g1{inward(To, From) * length}; + auto const count{std::max(4, static_cast(std::ceil(length)))}; + for (int i = 1; i < count; ++i) + { + auto const u{static_cast(i) / count}; + auto const u2{u * u}; + auto const u3{u2 * u}; + auto const height{(2.0 * u3 - 3.0 * u2 + 1.0) * h0 + (u3 - 2.0 * u2 + u) * g0 + (-2.0 * u3 + 3.0 * u2) * h1 + (u3 - u2) * g1}; + if (std::abs(height - elevation(Line, From + u * length)) > Tolerance) + return false; + } + return true; + }; + std::vector result; + for (std::size_t k = 0; k + 1 < Joints.size(); ++k) + { + std::vector stops; + for (auto const chainage : all) + if (chainage > Joints[k] + 1e-6 && chainage < Joints[k + 1] - 1e-6) + stops.push_back(chainage); + stops.push_back(Joints[k + 1]); + auto from{Joints[k]}; + std::size_t next{0}; + while (next + 1 < stops.size()) + { + auto reach{next}; + for (auto i{next + 1}; i < stops.size() && follows(from, stops[i]); ++i) + reach = i; + if (reach + 1 == stops.size()) + break; + result.push_back(stops[reach]); + from = stops[reach]; + next = reach + 1; + } + } + return result; +} + std::string format_chainage(double const Chainage) { auto const value{std::abs(Chainage)}; diff --git a/editor/editorProfile.hpp b/editor/editorProfile.hpp index 5f1c42f7..9380e0d4 100644 --- a/editor/editorProfile.hpp +++ b/editor/editorProfile.hpp @@ -179,6 +179,9 @@ std::vector bridged(std::vector const &Chainages, std::vector // chainages where the paths have to end so that each of them carries a single grade or a single curve std::vector breaks(line const &Line); +// of the breaks those the paths between Joints can't do without: a path from one joint or break kept to the next follows the line +// within Tolerance with the heights and the grades of its ends +std::vector fewest_breaks(line const &Line, std::vector Joints, double const Tolerance); std::string format_chainage(double const Chainage); diff --git a/editor/editorSettings.cpp b/editor/editorSettings.cpp index 8c22d39c..1fc92150 100644 --- a/editor/editorSettings.cpp +++ b/editor/editorSettings.cpp @@ -116,6 +116,8 @@ bool editorSettings::load() m_top_rails = static_cast(std::strtoul(value.c_str(), nullptr, 16)) | 0xff000000u; else if (key == "track_norms") m_norms = value == "free" ? track_norms::free_laying : value == "plk" ? track_norms::plk : track_norms::ask; + else if (key == "lay_heights") + m_heights = value == "points" ? lay_heights::points : value == "level" ? lay_heights::level : lay_heights::fitted; else if (key.rfind(origin_prefix, 0) == 0) { // northing;easting @@ -165,6 +167,7 @@ bool editorSettings::save() stream << "top_tracks " << m_top_tracks << "\n"; stream << "top_rails " << std::hex << (m_top_rails & 0xffffffu) << std::dec << "\n"; stream << "track_norms " << (m_norms == track_norms::free_laying ? "free" : m_norms == track_norms::plk ? "plk" : "ask") << "\n"; + stream << "lay_heights " << (m_heights == lay_heights::points ? "points" : m_heights == lay_heights::level ? "level" : "fitted") << "\n"; stream << std::fixed << std::setprecision(3); for (auto const &origin : m_orthophoto_origins) stream << origin.first << " " << origin.second.first << ";" << origin.second.second << "\n"; diff --git a/editor/editorSettings.hpp b/editor/editorSettings.hpp index e0e9f001..45a964b9 100644 --- a/editor/editorSettings.hpp +++ b/editor/editorSettings.hpp @@ -76,6 +76,16 @@ public: }; track_norms norms() const { return m_norms; } void norms(track_norms Norms) { m_norms = Norms; } + // the heights of the tracks laid on the ground: fitted to it as the tracks go onto the terrain, the points clicked on it with + // even grades between them, level + enum class lay_heights + { + fitted, + points, + level + }; + lay_heights heights() const { return m_heights; } + void heights(lay_heights Heights) { m_heights = Heights; } private: movement_scheme m_movement{movement_scheme::wsad}; @@ -92,6 +102,7 @@ private: int m_top_tracks{0}; // the railway in the top view: whole, without the ballast, the rails alone std::uint32_t m_top_rails{0xffff6e28}; // colour of the rails alone, as ImGui keeps it: ABGR track_norms m_norms{track_norms::ask}; + lay_heights m_heights{lay_heights::fitted}; }; // global editor settings instance diff --git a/lang/editor_pl.po b/lang/editor_pl.po index c3d704cf..55bb9cc6 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11502,3 +11502,27 @@ msgstr "Kolor szyn, żeby odcinały się od ortofotomapy i mapy" msgid "(c) OpenStreetMap contributors, geoportal.gov.pl" msgstr "(c) współtwórcy OpenStreetMap, geoportal.gov.pl" + +msgid "The grade line of the whole track laid in a row fitted to the terrain model, as the tracks go onto the terrain" +msgstr "Pochylenia całego toru kładzionego za jednym razem dopasowane do modelu terenu, tak jak w „Tory na teren”" + +msgid "Height of the track on the ground" +msgstr "Wysokość toru na terenie" + +msgid "Fitted" +msgstr "Dopasowana" + +msgid "Point to point" +msgstr "Punkt–punkt" + +msgid "Flat" +msgstr "Poziomo" + +msgid "The grades fitted to the ground as Tracks onto the terrain fits them: vertical curves of the norms, the bumps of the ground gone past, the earthworks made by the terrain. On the terrain model the whole track laid in a row is fitted again at each click" +msgstr "Pochylenia dopasowane do terenu tak jak w „Tory na teren”: łuki pionowe według norm, drobne nierówności terenu pominięte, nasypy i wykopy robi teren. Na modelu terenu (NMT) cały tor kładziony za jednym razem jest dopasowywany od nowa przy każdym kliknięciu" + +msgid "Each point clicked at the height of the ground, an even grade between them: the grade changes only where you click" +msgstr "Każdy kliknięty punkt na wysokości terenu, między punktami równe pochylenie: pochylenie zmienia się tylko tam, gdzie klikniesz" + +msgid "The track goes on at the height it's laid from, whatever the ground; the first point on the ground" +msgstr "Tor idzie dalej na wysokości, z której go kładziesz, bez względu na teren; pierwszy punkt na terenie"