From 572daeb274e14ca04128c5a4a513f4e072fc4563 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 16:06:23 +0200 Subject: [PATCH] feat(editor): the horizontal alignment designed in the route design kept in the scenery as //$h lines: its vertices and the elements they make (straight, transition, arc, with the radius at the start and the end and the cant), so the paths laid by it are known again without guessing from their curvature; a curve read from the paths takes the radius, the transitions, the cant and the compound arcs of the kept design when its paths lie on it within 2 cm, the other way round too; the whole line design taken back when the fragment has its ends; a curve redesigned alone taken into the line it belongs to; km of the start kept with the design and carried on to the vertical profile of the same paths; paths which no longer follow the kept design shown in the route window, with the kept design to take or to forget; self-test commands for the route design and the kept alignments; Polish translation --- CMakeLists.txt | 3 +- application/editormode.cpp | 2 + application/editormode.h | 10 +- application/editormodeprofile.cpp | 10 + application/editormodeselftest.cpp | 207 ++++++++++ application/editormodetrack.cpp | 117 +++++- editor/editorAlignmentStore.cpp | 610 +++++++++++++++++++++++++++++ editor/editorAlignmentStore.hpp | 91 +++++ editor/editorTrack.cpp | 21 + editor/editorTrack.hpp | 4 + lang/editor_pl.po | 33 ++ scene/scenelayers.cpp | 2 +- 12 files changed, 1105 insertions(+), 5 deletions(-) create mode 100644 editor/editorAlignmentStore.cpp create mode 100644 editor/editorAlignmentStore.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 62b41aa5..7d6b7d24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -178,6 +178,7 @@ set(SOURCES "editor/editorTrack.cpp" "editor/editorRoad.cpp" "editor/editorAlignment.cpp" +"editor/editorAlignmentStore.cpp" "editor/editorSpeed.cpp" "editor/editorProfile.cpp" "editor/editorGauge.cpp" @@ -698,4 +699,4 @@ if(ENABLE_AVX2) else() target_compile_options(${PROJECT_NAME} PRIVATE -mavx2) endif() -endif() \ No newline at end of file +endif() diff --git a/application/editormode.cpp b/application/editormode.cpp index be970a88..134c5335 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -28,6 +28,7 @@ extern char **environ; #include "editor/editorIncludeInfo.hpp" #include "editor/editorGroundMesh.hpp" #include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIcons.hpp" #include "utilities/translation.h" @@ -495,6 +496,7 @@ void editor_mode::editormode_input::poll() } editor_mode::editor_mode() { + alignment_store::reset(); m_userinterface = std::make_shared(); // the area fill settings live in the node bank window, in the tab of the fill mode ui()->set_fill_options([this]() { render_area_fill(); }); diff --git a/application/editormode.h b/application/editormode.h index 2c6d65e9..015497bd 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -769,6 +769,10 @@ class editor_mode : public application_mode, private editor_track::observer int vertex{-1}; int grip{-1}; bool single{true}; // one curve between its straights, instead of the whole line + double origin{0.0}; // km of the start, m + int designed{0}; // vertices whose parameters came from the alignment kept in the scenery + int departed{-1}; // alignment kept in the scenery near the fragment, which its paths no longer follow + double departure{0.0}; }; void render_route_ui(); bool render_route_parameters(); @@ -780,7 +784,11 @@ class editor_mode : public application_mode, private editor_track::observer bool route_from_curve(TTrack &Track); bool route_from_line(TTrack &Track); alignment::vertex route_vertex_of(editor_track::curve const &Curve) const; - bool route_load(TTrack &Track) { return m_route.single ? route_from_curve(Track) : route_from_line(Track); } + bool route_load(TTrack &Track); + void route_read_stored(); + void route_take_stored(int const Index); + void route_store(); + void render_route_stored(); void route_update() { m_route.result = alignment::compute(m_route.design); } void route_apply(); bool route_relay_through_switches(editor_track::chain const &Chain, std::vector> &States, std::vector &Created); diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 4fafe1d3..eb10e686 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormode.h" #include "application/editorprojection.h" #include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" #include "editor/editorGeometry.hpp" #include "world/Track.h" @@ -390,7 +391,16 @@ void editor_mode::profile_take(editor_track::route Route, TTrack *From, TTrack * state.selected = -1; profile_resample(); if (false == profile_restore()) + { profile_recognize(); + if (false == state.samples.empty()) + { + auto const km{alignment_store::chainage_at(state.samples.front().position)}; + auto const further{alignment_store::chainage_at(editor_track::sampled_position(state.samples, std::min(state.route.length, 5.0)))}; + if (km.has_value() && further.has_value() && *further > *km) + state.origin = *km; + } + } profile_fit_view(); } diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 07929123..50558c8c 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -33,6 +33,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" #include #include @@ -92,6 +93,11 @@ http://mozilla.org/MPL/2.0/. // layto from the free end at the first point towards the second, as a drag in the layout // layclicks ... clicks of the lay tool at the points, a free end within 3 m joined // joints ... the joints check over the scenery, the issues around the points logged +// route the route design of the path nearest the point, a curve or the whole line +// routevertex , routearc , routeshare , routekm +// routeapply, routetake as the buttons of the route design do it +// alignments logs the alignments kept in the scenery and the element under each path +// splitkind , nudgekind the first path over an element of the kind split in two, or moved // screenshot // save // log @@ -977,6 +983,207 @@ void editor_mode::selftest_step() else WriteLog("SELFTEST gauge " + name + ": no such track"); } + else if (command == "route") + { + glm::dvec3 point{0.0}; + int single{1}; + words >> point.x >> point.z >> single; + TTrack *nearest{nullptr}; + double distance{1e9}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)}; + if (d < distance) + { + distance = d; + nearest = track; + } + } + m_track_tab = track_tab::route; + m_route.single = (single != 0); + auto const loaded{nearest != nullptr && route_load(*nearest)}; + WriteLog(format("SELFTEST route: %s from %s, %zu paths, %.3f m, %zu vertices, %d from the design, km %.3f, departed %d by %.3f m, valid %d", loaded ? "loaded" : "failed", nearest != nullptr ? nearest->name().c_str() : "-", + m_route.chain.tracks.size(), m_route.chain.length, m_route.design.vertices.size(), m_route.designed, m_route.origin / 1000.0, m_route.departed, m_route.departure, m_route.result.valid ? 1 : 0)); + WriteLog("SELFTEST route status: " + m_route.status + " " + m_route.error); + for (std::size_t i = 0; i < m_route.design.vertices.size(); ++i) + { + auto const &v{m_route.design.vertices[i]}; + auto text{format("SELFTEST route vertex %zu: at %.3f %.3f offset %.3f R %.3f L %.3f / %.3f cant %.1f compound %d share %.4f", i, v.position.x, v.position.y, v.offset, v.radius, v.transition_in, v.transition_out, v.cant, v.compound ? 1 : 0, v.share)}; + for (auto const &arc : v.arcs) + text += format(" [R %.3f L %.3f share %.4f]", arc.radius, arc.transition, arc.share); + WriteLog(text); + } + } + else if (command == "routevertex" || command == "routearc" || command == "routeshare") + { + int index{0}; + words >> index; + if (index < 0 || index >= static_cast(m_route.design.vertices.size())) + { + WriteLog("SELFTEST " + command + ": no vertex " + std::to_string(index)); + continue; + } + auto &vertex{m_route.design.vertices[index]}; + if (command == "routevertex") + words >> vertex.radius >> vertex.transition_in >> vertex.transition_out >> vertex.cant; + else if (command == "routeshare") + words >> vertex.share; + else + { + alignment::compound_arc arc; + words >> arc.radius >> arc.transition >> arc.share; + vertex.compound = true; + vertex.arcs.push_back(arc); + } + route_update(); + std::string errors; + for (auto const &error : m_route.result.errors) + errors += " " + error; + WriteLog(format("SELFTEST %s: valid %d, %zu elements, extension %.3f / %.3f", command.c_str(), m_route.result.valid ? 1 : 0, m_route.result.elements.size(), m_route.result.start_extension, m_route.result.end_extension) + errors); + } + else if (command == "routekm") + { + double km{0.0}; + words >> km; + m_route.origin = km * 1000.0; + } + else if (command == "routeapply" || command == "routetake") + { + if (command == "routeapply") + route_apply(); + else + route_take_stored(m_route.departed); + WriteLog("SELFTEST " + command + ": " + m_route.status + " " + m_route.error); + } + else if (command == "alignments") + { + auto const &records{alignment_store::records()}; + for (std::size_t r = 0; r < records.size(); ++r) + { + auto const &entry{records[r]}; + WriteLog(format("SELFTEST alignment %zu in %s: km %.3f, %zu vertices, %zu elements, consistent %d, length %.3f", r, scene::Layers.layer(entry.layer).name.c_str(), entry.origin / 1000.0, entry.design.vertices.size(), entry.elements.size(), entry.consistent ? 1 : 0, entry.result.length)); + for (auto const &e : entry.elements) + WriteLog(format("SELFTEST element %s vertex %d km %.3f L %.3f R %.3f -> %.3f cant %.1f", e.kind.c_str(), e.vertex, (entry.origin + e.chainage) / 1000.0, e.length, e.radius_start, e.radius_end, e.cant)); + } + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const found{alignment_store::on_track(*track)}; + std::string text{"SELFTEST path " + track->name() + ": "}; + if (false == found.has_value()) + { + auto const closest{alignment_store::at(geometry::bezier{track->m_paths.front()}.point(0.5), 1000.0)}; + text += closest.has_value() ? format("off the alignments, %.3f m from alignment %d", closest->offset, closest->record) : std::string{"off the alignments"}; + } + else + { + auto const e{alignment_store::describe(records[found->record].result.elements[found->element])}; + text += format("alignment %d element %d %s R %.3f -> %.3f, offset %.4f", found->record, found->element, e.kind.c_str(), e.radius_start, e.radius_end, found->offset); + } + editor_track::curve curve; + if (editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) + text += format(" | curve R %.3f L %.3f / %.3f cant %.1f compound %d arcs %zu %s", curve.radius, curve.transition_in, curve.transition_out, curve.cant, curve.compound ? 1 : 0, curve.arcs.size(), curve.designed.has_value() ? "designed" : "recognized"); + WriteLog(text); + } + } + else if (command == "vprofile") + { + std::string from, to; + words >> from >> to; + auto *first{simulation::Paths.find(from)}; + auto *last{simulation::Paths.find(to)}; + if (first == nullptr || last == nullptr) + { + WriteLog("SELFTEST vprofile: no such paths"); + continue; + } + m_profile.origin = 0.0; + profile_open(first, last); + WriteLog(format("SELFTEST vprofile: %zu spans, %.3f m, km of the start %.3f", m_profile.route.spans.size(), m_profile.route.length, m_profile.origin / 1000.0)); + } + else if (command == "backwardcurve") + { + std::string name; + words >> name; + auto *track{simulation::Paths.find(name)}; + editor_track::curve curve; + editor_track::chain chain; + std::string error; + if (track == nullptr || false == editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve) || false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) + { + WriteLog("SELFTEST backwardcurve: no curve at " + name); + continue; + } + std::vector> run; + for (std::size_t i = chain.tracks.size(); i > 0; --i) + run.emplace_back(chain.tracks[i - 1], false == chain.forward[i - 1]); + editor_track::curve backward; + editor_track::analyse_curve(run, m_route.design.norms.gauge, backward); + auto text{format("SELFTEST backwardcurve: %zu paths, R %.3f L %.3f / %.3f cant %.1f compound %d %s", run.size(), backward.radius, backward.transition_in, backward.transition_out, backward.cant, backward.compound ? 1 : 0, backward.designed.has_value() ? "designed" : "recognized")}; + for (auto const &arc : backward.arcs) + text += format(" [R %.3f turn %.4f L %.3f]", arc.radius, arc.turn, arc.transition); + WriteLog(text); + } + else if (command == "splitkind" || command == "nudgekind") + { + std::string kind; + glm::dvec3 offset{0.0}; + words >> kind >> offset.x >> offset.z; + auto const &records{alignment_store::records()}; + TTrack *chosen{nullptr}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const found{alignment_store::on_track(*track)}; + if (found.has_value() && alignment_store::describe(records[found->record].result.elements[found->element]).kind == kind) + { + chosen = track; + break; + } + } + if (chosen == nullptr) + { + WriteLog("SELFTEST " + command + ": no path over " + kind); + continue; + } + if (command == "splitkind") + { + auto const before{editor_track::capture(*chosen)}; + auto *created{editor_track::split_path(*chosen, 0.5)}; + if (created != nullptr) + push_track_snapshot({{chosen, before}}, {created}); + WriteLog("SELFTEST splitkind: " + chosen->name() + " split, the second half " + (created != nullptr ? created->name() : std::string{"-"})); + } + else + { + std::vector> states{{chosen, editor_track::capture(*chosen)}}; + std::vector moved{chosen}; + auto const &path{chosen->m_paths.front()}; + for (auto const &end : {path.points[segment_data::point::start], path.points[segment_data::point::end]}) + for (auto *neighbour : {chosen->trPrev, chosen->trNext}) + { + if (neighbour == nullptr || neighbour->m_paths.empty() || std::find(moved.begin(), moved.end(), neighbour) != moved.end()) + continue; + auto const &other{neighbour->m_paths.front()}; + for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end}) + if (glm::distance(other.points[kind == editor_track::point_kind::start ? segment_data::point::start : segment_data::point::end], end) < 0.01) + { + states.emplace_back(neighbour, editor_track::capture(*neighbour)); + editor_track::move_point(*neighbour, {0, kind}, end + offset); + moved.push_back(neighbour); + break; + } + } + editor_track::translate(*chosen, offset); + editor_track::commit(moved); + push_track_snapshot(std::move(states), {}); + WriteLog("SELFTEST nudgekind: " + chosen->name() + " moved with " + std::to_string(moved.size() - 1) + " joined ends"); + } + } else if (command == "join" || command == "layto") { glm::dvec3 a{0.0}, b{0.0}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 9cf99544..c5a0474c 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -13,6 +13,7 @@ http: #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" @@ -2411,6 +2412,7 @@ bool editor_mode::route_from_curve(TTrack &Track) return false; auto &design{m_route.design}; auto vertex{route_vertex_of(curve)}; + m_route.designed = curve.designed.has_value() ? 1 : 0; if (curve.reversals == 0) { design.vertices = {vertex}; @@ -2430,6 +2432,8 @@ bool editor_mode::route_from_curve(TTrack &Track) alignment::vertex editor_mode::route_vertex_of(editor_track::curve const &Curve) const { + if (Curve.designed.has_value()) + return *Curve.designed; alignment::vertex vertex; vertex.radius = Curve.radius > 0.0 ? std::max(10.0, std::round(Curve.radius)) : 1000.0; route_recommend(vertex); @@ -2444,6 +2448,110 @@ alignment::vertex editor_mode::route_vertex_of(editor_track::curve const &Curve) return vertex; } +bool editor_mode::route_load(TTrack &Track) +{ + m_route.designed = 0; + if (false == (m_route.single ? route_from_curve(Track) : route_from_line(Track))) + return false; + route_read_stored(); + return true; +} + +// the design kept in the scenery for the fragment takes the place of the one read from the paths; its chainage is carried on +void editor_mode::route_read_stored() +{ + auto &route{m_route}; + route.departed = -1; + route.departure = 0.0; + route.origin = 0.0; + if (route.chain.tracks.empty()) + return; + auto const &chain{route.chain}; + bool reversed; + auto const index{alignment_store::find(chain.start, chain.end, reversed)}; + if (index >= 0 && false == reversed && false == route.single) + { + route_take_stored(index); + } + else if (auto const km{alignment_store::chainage_at(chain.start)}) + { + auto const ahead{chain.start + glm::dvec3{route.design.start_direction.x, 0.0, route.design.start_direction.y}}; + auto const further{alignment_store::chainage_at(ahead)}; + if (further.has_value() && *further > *km) + route.origin = *km; + } + route.departed = alignment_store::departed(chain.tracks, route.departure); + if (route.designed > 0) + route.status += format(STR_C("\nCurves read from the design kept in the scenery: %d"), route.designed); + else if (false == route.design.vertices.empty()) + route.status += STR_C("\nCurves recognized from the paths, the scenery keeps no design for them"); +} + +void editor_mode::route_take_stored(int const Index) +{ + auto &route{m_route}; + auto const &records{alignment_store::records()}; + if (Index < 0 || Index >= static_cast(records.size())) + return; + auto const &stored{records[Index]}; + auto &design{route.design}; + design.vertices = stored.design.vertices; + design.shape = stored.design.shape; + design.speed = stored.design.speed; + design.norms = stored.design.norms; + design.transition_pieces = stored.design.transition_pieces; + design.arc_piece_angle = stored.design.arc_piece_angle; + route.designed = static_cast(design.vertices.size()); + route.origin = stored.origin; + route.vertex = route.grip = -1; + route_update(); +} + +void editor_mode::render_route_stored() +{ + auto &route{m_route}; + ImGui::PushItemWidth(120.0f); + auto km{route.origin / 1000.0}; + if (ImGui::InputDouble(STR_C("Km of the start"), &km, 0.0, 0.0, "%.3f")) + route.origin = km * 1000.0; + ImGui::PopItemWidth(); + item_tooltip("Chainage of the start of the fragment, kept with the design in the scenery; the vertical profile of these paths counts from it"); + auto const &records{alignment_store::records()}; + if (route.departed < 0 || route.departed >= static_cast(records.size())) + return; + auto const &layer{scene::Layers.layer(records[route.departed].layer).name}; + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), STR_C("The design kept in %s for this fragment departs from its paths by up to %.2f m"), layer.c_str(), route.departure); + if (ImGui::SmallButton(STR_C("Take the kept design"))) + { + route_take_stored(route.departed); + route.status = STR_C("The kept design is loaded, applying it lays the paths by it again"); + } + item_tooltip("Loads the design kept in the scenery in place of the one read from the paths; nothing changes until it's applied"); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Forget the kept design"))) + { + alignment_store::forget(route.departed); + route.departed = -1; + route.status = STR_C("The kept design will be removed from ") + layer + STR_C(" on save"); + } + item_tooltip("The paths stay as they are, the scenery no longer keeps a design for them"); +} + +void editor_mode::route_store() +{ + auto &route{m_route}; + if (false == route.result.valid || route.chain.tracks.empty() || scene::Layers.empty()) + return; + auto layer{scene::Layers.resolve(route.chain.tracks.front()->layer())}; + if (false == scene::Layers.valid(layer) || false == scene::Layers.accepts(layer)) + layer = scene::Layers.active(); + if (false == scene::Layers.valid(layer)) + layer = 1; + alignment_store::store(route.design, route.result, route.origin, layer); + route.departed = -1; + route.departure = 0.0; +} + bool editor_mode::route_from_line(TTrack &Track) { auto &route{m_route}; @@ -2457,6 +2565,7 @@ bool editor_mode::route_from_line(TTrack &Track) return false; auto &design{route.design}; auto const found{editor_track::recognize_line(route.chain, m_straights.tolerance, design.norms.gauge)}; + route.designed = 0; if (false == found.empty()) { design.vertices.clear(); @@ -2471,6 +2580,8 @@ bool editor_mode::route_from_line(TTrack &Track) else { vertex = route_vertex_of(entry.shape); + if (entry.shape.designed.has_value()) + ++route.designed; } vertex.position = entry.position; design.vertices.push_back(vertex); @@ -2583,6 +2694,7 @@ void editor_mode::route_apply() } route_update(); } + route_store(); route.status = STR_C("Re-laid ") + std::to_string(chain.tracks.size()) + STR_C(" paths") + (added > 0 ? STR_C(", added ") + std::to_string(added) : std::string{}); if (startextension > 0.0 || endextension > 0.0) route.status += format(", the curve took over %.2f / %.2f m of the adjoining straights", startextension, endextension); @@ -2935,6 +3047,7 @@ void editor_mode::render_route_ui() return; } item_tooltip("Reads the curves of the line from the scenery again, dropping the changes not applied"); + render_route_stored(); auto changed{render_route_vertices()}; if (ImGui::CollapsingHeader(STR_C("Design speed and limits"))) changed |= render_route_parameters(); @@ -3115,9 +3228,9 @@ void editor_mode::render_route_result() if (ImGui::TreeNode(STR_C("Elements"))) { ImGui::TextDisabled(STR_C("Straight available beyond the ends: %.2f / %.2f m"), design.start_reserve, design.end_reserve); - char const *kinds[] = {"straight", "transition", "curve", "transition"}; + char const *kinds[] = {"straight", "transition", "curve", "transition", "spiral"}; for (auto const &e : result.elements) - ImGui::Text("%8.2f m %-10s L %8.2f m%s", e.chainage, kinds[static_cast(e.kind)], e.length, e.kind == alignment::element_kind::straight ? "" : (" R " + std::to_string(static_cast(std::round(e.radius))) + " m").c_str()); + ImGui::Text("km %s %-10s L %8.2f m%s", profile::format_chainage(m_route.origin + e.chainage).c_str(), kinds[static_cast(e.kind)], e.length, e.kind == alignment::element_kind::straight ? "" : (" R " + std::to_string(static_cast(std::round(e.radius))) + " m").c_str()); ImGui::TreePop(); } diff --git a/editor/editorAlignmentStore.cpp b/editor/editorAlignmentStore.cpp new file mode 100644 index 00000000..64cc86f7 --- /dev/null +++ b/editor/editorAlignmentStore.cpp @@ -0,0 +1,610 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "world/Track.h" +#include "utilities/Logs.h" + +#include +#include +#include +#include +#include + +namespace alignment_store +{ + +namespace +{ + +using geometry::plan_of; + +std::string const kMark{"//$h"}; +double const kStep{1.0}; + +std::vector g_records; +bool g_loaded{false}; + +// format of the lines, coordinates in the space of the file: +// alignment +// source origin shape speed +// ends +// norms +// pieces +// vertex [ ]... +// element +// radii signed with the turn, 0 for no curvature +// end +char const *shape_name(alignment::transition_shape const Shape) +{ + return Shape == alignment::transition_shape::clothoid ? "clothoid" : "parabola"; +} + +glm::dvec2 local_direction(scene::layer_context const &Context, glm::dvec3 const &Point, glm::dvec2 const &Direction) +{ + auto const from{Context.to_local(Point)}; + auto const to{Context.to_local(Point + glm::dvec3{Direction.x, 0.0, Direction.y})}; + auto const direction{plan_of(to - from)}; + return glm::length(direction) > 1e-12 ? glm::normalize(direction) : Direction; +} + +glm::dvec2 world_direction(scene::layer_context const &Context, glm::dvec3 const &Local, glm::dvec2 const &Direction) +{ + auto const from{Context.to_world(Local)}; + auto const to{Context.to_world(Local + glm::dvec3{Direction.x, 0.0, Direction.y})}; + auto const direction{plan_of(to - from)}; + return glm::length(direction) > 1e-12 ? glm::normalize(direction) : Direction; +} + +void sample(record &Record) +{ + Record.samples.clear(); + auto const length{Record.result.length}; + auto const count{static_cast(std::ceil(length / kStep))}; + for (std::size_t i = 0; i <= count; ++i) + Record.samples.push_back(plan_of(alignment::evaluate(Record.result, std::min(length, i * kStep)).position)); + Record.low = Record.high = Record.samples.empty() ? glm::dvec2{0.0} : Record.samples.front(); + for (auto const &point : Record.samples) + { + Record.low = glm::min(Record.low, point); + Record.high = glm::max(Record.high, point); + } +} + +bool agrees(std::vector const &Stored, std::vector const &Made) +{ + if (Stored.size() != Made.size()) + return false; + for (std::size_t i = 0; i < Stored.size(); ++i) + { + auto const made{describe(Made[i])}; + if (Stored[i].kind != made.kind || std::abs(Stored[i].chainage - made.chainage) > 0.01 || std::abs(Stored[i].length - made.length) > 0.01 || + std::abs(Stored[i].radius_start - made.radius_start) > 0.01 || std::abs(Stored[i].radius_end - made.radius_end) > 0.01) + return false; + } + return true; +} + +void mark(scene::layer_handle const Layer) +{ + if (false == scene::Layers.valid(Layer)) + return; + auto const &context{scene::Layers.layer(Layer).context_insert()}; + auto const local = [&](glm::dvec3 const &Point) { + auto const point{context.to_local(Point)}; + return format("%.4f %.4f %.4f", point.x, point.y, point.z); + }; + std::vector lines; + for (auto const &entry : g_records) + { + if (entry.layer != Layer) + continue; + if (lines.empty()) + lines.emplace_back("# horizontal alignments designed in the track editor, the simulation skips these lines"); + auto const &design{entry.design}; + lines.push_back("alignment " + local(design.start) + ' ' + local(design.end)); + lines.push_back(format("source %s origin %.4f shape %s speed %.1f", entry.source.c_str(), entry.origin, shape_name(design.shape), design.speed)); + auto const startdirection{local_direction(context, design.start, design.start_direction)}; + auto const enddirection{local_direction(context, design.end, design.end_direction)}; + lines.push_back(format("ends %.9f %.9f %.9f %.9f %.6f %.6f %.3f %.3f", startdirection.x, startdirection.y, enddirection.x, enddirection.y, design.start_grade, design.end_grade, design.start_radius, design.end_radius)); + auto const &norms{design.norms}; + lines.push_back(format("norms %.1f %.3f %.1f %.1f %.2f %.3f %.2f %.2f %.3f", norms.gauge, norms.unbalanced, norms.cant_max, norms.cant_rate, norms.ramp_factor, norms.jerk, norms.tangent_min_time, norms.arc_min, norms.arc_min_factor)); + lines.push_back(format("pieces %d %.3f", design.transition_pieces, design.arc_piece_angle)); + for (auto const &vertex : design.vertices) + { + auto const position{context.to_local({vertex.position.x, 0.0, vertex.position.y})}; + auto text{format("vertex %.4f %.4f %.4f %.3f %.3f %.3f %.1f %d %d %.9f %zu", position.x, position.z, vertex.offset, vertex.radius, vertex.transition_in, vertex.transition_out, vertex.cant, vertex.reverse_turn ? 1 : 0, vertex.compound ? 1 : 0, + vertex.share, vertex.arcs.size())}; + for (auto const &arc : vertex.arcs) + text += format(" %.3f %.3f %.9f", arc.radius, arc.transition, arc.share); + lines.push_back(text); + } + for (auto const &e : entry.result.elements) + { + auto const station{alignment::evaluate(entry.result, e.chainage)}; + auto const start{context.to_local(station.position)}; + auto const direction{local_direction(context, station.position, glm::normalize(plan_of(station.direction)))}; + auto const element{describe(e)}; + lines.push_back(format("element %s %d %.4f %.4f %.3f %.3f %.1f %.4f %.4f %.9f %.9f", element.kind.c_str(), element.vertex, element.chainage, element.length, element.radius_start, element.radius_end, element.cant, start.x, start.z, direction.x, direction.y)); + } + lines.emplace_back("end"); + } + scene::Layers.mark(Layer, kMark, std::move(lines)); +} + +// the vertex as seen from the other end of the line +alignment::vertex reversed_vertex(alignment::vertex Vertex) +{ + std::swap(Vertex.transition_in, Vertex.transition_out); + if (Vertex.compound && false == Vertex.arcs.empty()) + { + struct part + { + double radius, share, transition; + }; + std::vector parts{{Vertex.radius, Vertex.share, 0.0}}; + for (auto const &arc : Vertex.arcs) + parts.push_back({arc.radius, arc.share, arc.transition}); + auto const count{parts.size()}; + Vertex.radius = parts.back().radius; + Vertex.share = parts.back().share; + for (std::size_t j = 1; j < count; ++j) + Vertex.arcs[j - 1] = {parts[count - 1 - j].radius, parts[count - j].transition, parts[count - 1 - j].share}; + } + return Vertex; +} + +void finish(record &Record) +{ + Record.result = alignment::compute(Record.design); + sample(Record); +} + +// the curves of a design whose vertices are vertices of a stored alignment are taken into that alignment, in place of its own +int merge(record const &Entry) +{ + auto const &vertices{Entry.result.vertices}; + if (vertices.empty()) + return -1; + for (std::size_t r = 0; r < g_records.size(); ++r) + { + auto const &others{g_records[r].result.vertices}; + if (others.size() < vertices.size()) + continue; + for (auto const backwards : {false, true}) + for (std::size_t first = 0; first + vertices.size() <= others.size(); ++first) + { + auto const other = [&](std::size_t const K) { return backwards ? first + vertices.size() - 1 - K : first + K; }; + bool same{true}; + for (std::size_t k = 0; k < vertices.size() && same; ++k) + same = glm::distance(vertices[k], others[other(k)]) <= 0.1; + if (false == same) + continue; + auto merged{g_records[r]}; + for (std::size_t k = 0; k < vertices.size(); ++k) + { + auto const shape{backwards ? reversed_vertex(Entry.design.vertices[k]) : Entry.design.vertices[k]}; + auto &target{merged.design.vertices[other(k)]}; + auto const position{target.position}; + auto const offset{target.offset}; + target = shape; + target.position = position; + target.offset = offset; + } + finish(merged); + if (false == merged.result.valid) + return -1; + merged.elements.clear(); + for (auto const &e : merged.result.elements) + merged.elements.push_back(describe(e)); + merged.consistent = true; + g_records[r] = std::move(merged); + return static_cast(r); + } + } + return -1; +} + +} // namespace + +stored_element describe(alignment::element const &Element) +{ + stored_element result; + result.vertex = Element.vertex; + result.chainage = Element.chainage; + result.length = Element.length; + result.cant = Element.cant; + auto const sign{Element.turn < 0 ? -1.0 : 1.0}; + auto const radius = [&](double const Curvature) { return Curvature > 1e-12 ? sign / Curvature : 0.0; }; + switch (Element.kind) + { + case alignment::element_kind::transition_in: + result.radius_end = sign * Element.radius; + break; + case alignment::element_kind::arc: + result.radius_start = result.radius_end = sign * Element.radius; + break; + case alignment::element_kind::transition_out: + result.radius_start = sign * Element.radius; + break; + case alignment::element_kind::spiral: + result.radius_start = radius(Element.curvature_start); + result.radius_end = radius(Element.curvature_end); + break; + default: + result.cant = 0.0; + break; + } + if (result.radius_start == 0.0 && result.radius_end == 0.0) + result.kind = "straight"; + else if (std::abs(result.radius_start - result.radius_end) < 1e-6) + result.kind = "arc"; + else + result.kind = "transition"; + return result; +} + +void reset() +{ + g_records.clear(); + g_loaded = false; +} + +void load() +{ + if (g_loaded || scene::Layers.empty()) + return; + g_loaded = true; + g_records.clear(); + for (auto const layer : scene::Layers.marked_layers(kMark)) + { + auto const &context{scene::Layers.layer(layer).context_insert()}; + std::optional current; + std::vector made; + for (auto const &text : scene::Layers.marked(layer, kMark)) + { + std::istringstream line{text}; + std::string keyword, word; + line >> keyword; + if (keyword == "alignment") + { + record entry; + entry.layer = layer; + glm::dvec3 start, end; + line >> start.x >> start.y >> start.z >> end.x >> end.y >> end.z; + if (line.fail()) + { + current.reset(); + continue; + } + entry.design.start = context.to_world(start); + entry.design.end = context.to_world(end); + current = entry; + continue; + } + if (false == current.has_value()) + continue; + auto &entry{*current}; + auto &design{entry.design}; + if (keyword == "source") + { + std::string shape; + line >> entry.source >> word >> entry.origin >> word >> shape >> word >> design.speed; + design.shape = shape == "clothoid" ? alignment::transition_shape::clothoid : alignment::transition_shape::cubic_parabola; + } + else if (keyword == "ends") + { + glm::dvec2 startdirection, enddirection; + line >> startdirection.x >> startdirection.y >> enddirection.x >> enddirection.y >> design.start_grade >> design.end_grade >> design.start_radius >> design.end_radius; + design.start_direction = world_direction(context, context.to_local(design.start), startdirection); + design.end_direction = world_direction(context, context.to_local(design.end), enddirection); + } + else if (keyword == "norms") + { + auto &norms{design.norms}; + line >> norms.gauge >> norms.unbalanced >> norms.cant_max >> norms.cant_rate >> norms.ramp_factor >> norms.jerk >> norms.tangent_min_time >> norms.arc_min >> norms.arc_min_factor; + } + else if (keyword == "pieces") + { + line >> design.transition_pieces >> design.arc_piece_angle; + } + else if (keyword == "vertex") + { + alignment::vertex vertex; + glm::dvec3 position{0.0}; + int reverse{0}, compound{0}; + std::size_t arcs{0}; + line >> position.x >> position.z >> vertex.offset >> vertex.radius >> vertex.transition_in >> vertex.transition_out >> vertex.cant >> reverse >> compound >> vertex.share >> arcs; + for (std::size_t i = 0; i < arcs && false == line.fail(); ++i) + { + alignment::compound_arc arc; + line >> arc.radius >> arc.transition >> arc.share; + vertex.arcs.push_back(arc); + } + if (line.fail()) + continue; + position = context.to_world(position); + vertex.position = {position.x, position.z}; + vertex.reverse_turn = (reverse != 0); + vertex.compound = (compound != 0); + design.vertices.push_back(vertex); + } + else if (keyword == "element") + { + stored_element e; + line >> e.kind >> e.vertex >> e.chainage >> e.length >> e.radius_start >> e.radius_end >> e.cant; + if (false == line.fail()) + entry.elements.push_back(e); + } + else if (keyword == "end") + { + finish(entry); + if (false == entry.result.valid || entry.result.elements.empty()) + { + WriteLog("Editor: a horizontal alignment in " + scene::Layers.layer(layer).name + " doesn't make a line, left out", logtype::generic); + current.reset(); + continue; + } + entry.consistent = agrees(entry.elements, entry.result.elements); + if (false == entry.consistent) + WriteLog("Editor: the elements of a horizontal alignment in " + scene::Layers.layer(layer).name + " differ from the ones its design makes now", logtype::generic); + g_records.push_back(std::move(entry)); + current.reset(); + } + } + } + if (false == g_records.empty()) + WriteLog("Editor: " + std::to_string(g_records.size()) + " horizontal alignment(s) read from the scenery", logtype::generic); +} + +std::vector const &records() +{ + load(); + return g_records; +} + +namespace +{ + +// the point on the line of the stored alignment, if it lies by it within the reach and not beyond its ends +std::optional project(std::size_t const Index, glm::dvec2 const &Point, double const Reach) +{ + auto const &entry{g_records[Index]}; + auto const margin{Reach + kStep}; + if (entry.samples.empty() || Point.x < entry.low.x - margin || Point.y < entry.low.y - margin || Point.x > entry.high.x + margin || Point.y > entry.high.y + margin) + return std::nullopt; + std::size_t nearest{0}; + double distance{std::numeric_limits::max()}; + for (std::size_t i = 0; i < entry.samples.size(); ++i) + { + auto const d{glm::distance(entry.samples[i], Point)}; + if (d < distance) + { + distance = d; + nearest = i; + } + } + if (distance > margin) + return std::nullopt; + auto const away = [&](double const S) { return glm::distance(plan_of(alignment::evaluate(entry.result, S).position), Point); }; + auto low{std::max(0.0, nearest * kStep - kStep)}; + auto high{std::min(entry.result.length, nearest * kStep + kStep)}; + for (int i = 0; i < 60; ++i) + { + auto const a{low + (high - low) / 3.0}; + auto const b{high - (high - low) / 3.0}; + if (away(a) < away(b)) + high = b; + else + low = a; + } + auto const chainage{(low + high) * 0.5}; + auto const offset{away(chainage)}; + if (offset > Reach) + return std::nullopt; + if (chainage < 1e-3 || chainage > entry.result.length - 1e-3) + { + auto const end{alignment::evaluate(entry.result, chainage)}; + auto const outward{glm::dot(Point - plan_of(end.position), glm::normalize(plan_of(end.direction))) * (chainage < 1e-3 ? -1.0 : 1.0)}; + if (outward > tolerance) + return std::nullopt; + } + auto const &elements{entry.result.elements}; + auto const found{std::upper_bound(elements.begin(), elements.end(), chainage, [](double const Value, alignment::element const &E) { return Value < E.chainage; })}; + return hit{static_cast(Index), static_cast(std::max(0, std::distance(elements.begin(), found) - 1)), chainage, offset}; +} + +} // namespace + +std::optional at(glm::dvec3 const &Point, double const Reach) +{ + load(); + std::optional best; + for (std::size_t r = 0; r < g_records.size(); ++r) + { + auto const found{project(r, plan_of(Point), Reach)}; + if (found.has_value() && (false == best.has_value() || found->offset < best->offset)) + best = found; + } + return best; +} + +std::optional on_track(TTrack const &Track, double const Reach) +{ + if (Track.m_paths.empty() || Track.m_editorremoved) + return std::nullopt; + load(); + geometry::bezier const curve{Track.m_paths.front()}; + std::optional best; + for (std::size_t r = 0; r < g_records.size(); ++r) + { + auto const middle{project(r, plan_of(curve.point(0.5)), Reach)}; + if (false == middle.has_value() || (best.has_value() && middle->offset >= best->offset)) + continue; + bool along{true}; + for (auto const t : {0.0, 0.25, 0.75, 1.0}) + along = along && project(r, plan_of(curve.point(t)), Reach).has_value(); + if (along) + best = middle; + } + return best; +} + +std::optional vertex_of(std::vector> const &Run) +{ + if (Run.empty() || records().empty()) + return std::nullopt; + auto const entering = [&](std::pair const &Member, bool const Atend) { + auto const &path{Member.first->m_paths.front()}; + return plan_of(path.points[(Member.second == Atend) ? segment_data::point::end : segment_data::point::start]); + }; + for (std::size_t r = 0; r < g_records.size(); ++r) + { + auto const &entry{g_records[r]}; + std::set vertices; + bool along{true}; + for (auto const &member : Run) + { + if (member.first == nullptr || member.first->m_paths.empty()) + return std::nullopt; + geometry::bezier const curve{member.first->m_paths.front()}; + std::vector hits; + for (auto const t : {0.0, 0.25, 0.5, 0.75, 1.0}) + if (auto const found{project(r, plan_of(curve.point(t)), tolerance)}) + hits.push_back(*found); + if (hits.size() < 5) + { + along = false; + break; + } + auto const from{std::min(hits.front().chainage, hits.back().chainage) + 0.01}; + auto const to{std::max(hits.front().chainage, hits.back().chainage) - 0.01}; + for (auto const &e : entry.result.elements) + if (e.kind != alignment::element_kind::straight && e.chainage < to && e.chainage + e.length > from && e.vertex >= 0) + vertices.insert(e.vertex); + } + if (false == along || vertices.size() != 1 || *vertices.begin() >= static_cast(entry.design.vertices.size())) + continue; + auto const start{project(r, entering(Run.front(), false), tolerance)}; + auto const end{project(r, entering(Run.back(), true), tolerance)}; + if (false == start.has_value() || false == end.has_value()) + continue; + designed_vertex result; + result.record = static_cast(r); + result.vertex = *vertices.begin(); + result.shape = entry.design.vertices[result.vertex]; + result.reversed = end->chainage < start->chainage; + if (result.reversed) + result.shape = reversed_vertex(result.shape); + return result; + } + return std::nullopt; +} + +int find(glm::dvec3 const &Start, glm::dvec3 const &End, bool &Reversed) +{ + load(); + Reversed = false; + for (std::size_t i = 0; i < g_records.size(); ++i) + { + auto const &design{g_records[i].design}; + if (geometry::plan_distance(design.start, Start) <= 0.1 && geometry::plan_distance(design.end, End) <= 0.1) + return static_cast(i); + if (geometry::plan_distance(design.start, End) <= 0.1 && geometry::plan_distance(design.end, Start) <= 0.1) + { + Reversed = true; + return static_cast(i); + } + } + return -1; +} + +int departed(std::vector const &Tracks, double &Departure) +{ + load(); + Departure = 0.0; + double const reach{5.0}; + int result{-1}; + for (auto *track : Tracks) + { + if (track == nullptr || track->m_paths.empty() || on_track(*track).has_value()) + continue; + geometry::bezier const curve{track->m_paths.front()}; + for (auto const t : {0.0, 0.25, 0.5, 0.75, 1.0}) + { + auto const found{at(curve.point(t), reach)}; + if (found.has_value() && found->offset > tolerance && found->offset > Departure) + { + Departure = found->offset; + result = found->record; + } + } + } + return result; +} + +int store(alignment::design const &Design, alignment::result const &Result, double const Origin, scene::layer_handle const Layer) +{ + load(); + record entry; + entry.design = Design; + entry.design.start_reserve = entry.design.end_reserve = 0.0; + entry.origin = Origin; + finish(entry); + if (false == entry.result.valid) + { + entry.result = Result; + sample(entry); + } + entry.elements.clear(); + for (auto const &e : entry.result.elements) + entry.elements.push_back(describe(e)); + bool reversed; + auto index{find(Design.start, Design.end, reversed)}; + if (index >= 0) + { + entry.layer = g_records[index].layer; + g_records[index] = std::move(entry); + } + else if ((index = merge(entry)) >= 0) + { + } + else + { + entry.layer = Layer; + g_records.push_back(std::move(entry)); + index = static_cast(g_records.size()) - 1; + } + mark(g_records[index].layer); + return index; +} + +void forget(int const Index) +{ + load(); + if (Index < 0 || Index >= static_cast(g_records.size())) + return; + auto const layer{g_records[Index].layer}; + g_records.erase(g_records.begin() + Index); + mark(layer); +} + +std::optional chainage_at(glm::dvec3 const &Point) +{ + auto const found{at(Point)}; + if (false == found.has_value()) + return std::nullopt; + return g_records[found->record].origin + found->chainage; +} + +} // namespace alignment_store diff --git a/editor/editorAlignmentStore.hpp b/editor/editorAlignmentStore.hpp new file mode 100644 index 00000000..1392f9d1 --- /dev/null +++ b/editor/editorAlignmentStore.hpp @@ -0,0 +1,91 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "editor/editorAlignment.hpp" +#include "scene/scenelayers.h" + +class TTrack; + +// horizontal alignments designed in the track editor, kept in the scenery files as "//$h" comment lines: the design (its +// vertices) and the elements it makes, so the tracks laid by it are known for straights, transition curves and arcs +namespace alignment_store +{ + +// an element as the file gives it: radii signed with the turn, 0 for no curvature +struct stored_element +{ + std::string kind; // straight, transition, arc + int vertex{-1}; + double chainage{0.0}; + double length{0.0}; + double radius_start{0.0}; + double radius_end{0.0}; + double cant{0.0}; +}; +stored_element describe(alignment::element const &Element); + +struct record +{ + scene::layer_handle layer{null_handle}; + std::string source{"designed"}; + double origin{0.0}; // chainage of the start, m + alignment::design design; + alignment::result result; // made of the design + std::vector elements; // as written in the file + bool consistent{true}; // the design makes the elements written in the file + glm::dvec2 low{0.0}, high{0.0}; + std::vector samples; +}; + +struct hit +{ + int record{-1}; + int element{-1}; + double chainage{0.0}; // from the start of the record + double offset{0.0}; // sideways from its line +}; + +// the designed vertex a curve made of the tracks follows: its parameters in the direction of the tracks +struct designed_vertex +{ + int record{-1}; + int vertex{-1}; + alignment::vertex shape; + bool reversed{false}; +}; + +double constexpr tolerance{0.02}; + +void reset(); +void load(); +std::vector const &records(); +std::optional at(glm::dvec3 const &Point, double const Reach = tolerance); +// the track lies along a stored alignment, its middle is the hit +std::optional on_track(TTrack const &Track, double const Reach = tolerance); +// the vertex the tracks follow, if they all lie along one stored alignment and the curve elements under them belong to one vertex +std::optional vertex_of(std::vector> const &Run); +// stored alignment with these ends, either way +int find(glm::dvec3 const &Start, glm::dvec3 const &End, bool &Reversed); +// stored alignment near the tracks, whose line they no longer follow; Departure receives the largest distance +int departed(std::vector const &Tracks, double &Departure); +// keeps the design in the file of the layer, in place of the one with the same ends +int store(alignment::design const &Design, alignment::result const &Result, double const Origin, scene::layer_handle const Layer); +void forget(int const Index); +// km of the point on the stored alignments, if it lies on one +std::optional chainage_at(glm::dvec3 const &Point); + +} // namespace alignment_store diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 99d3236e..24b3d79d 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -11,6 +11,7 @@ http: #include "editor/editorTrack.hpp" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" +#include "editor/editorAlignmentStore.hpp" #include "world/Track.h" #include "vehicle/DynObj.h" @@ -1756,6 +1757,26 @@ bool editor_track::analyse_curve(std::vector> const &R Curve.arcs = arcs; } } + if (Curve.reversals == 0) + { + if (auto const designed{alignment_store::vertex_of(Run)}) + { + auto const &shape{designed->shape}; + Curve.designed = shape; + Curve.radius = shape.radius; + Curve.transition_in = shape.transition_in; + Curve.transition_out = shape.transition_out; + Curve.cant = shape.cant; + Curve.compound = shape.compound && false == shape.arcs.empty(); + Curve.arcs.clear(); + if (Curve.compound) + { + Curve.arcs.push_back({shape.radius, shape.share, 0.0}); + for (auto const &arc : shape.arcs) + Curve.arcs.push_back({arc.radius, arc.share, arc.transition}); + } + } + } Curve.from = run.front().first; Curve.to = run.back().first; return true; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 8f8040aa..995219d5 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -11,12 +11,14 @@ http: #include #include +#include #include #include #include #include #include "world/Segment.h" +#include "editor/editorAlignment.hpp" #include "utilities/Classes.h" class TTrack; @@ -212,6 +214,8 @@ class editor_track double transition{0.0}; }; std::vector arcs; + // the parameters of the vertex in the alignment kept in the scenery, in the direction of the run, when the paths follow one + std::optional designed; }; static bool find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve); // curve made of the given paths, each with the flag of being run from its start diff --git a/lang/editor_pl.po b/lang/editor_pl.po index caade69d..85e289ab 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5313,3 +5313,36 @@ msgstr "pochylenie zmienione o %+.1f‰" msgid "cant %.0f mm (%+.0f mm)" msgstr "przechyłka %.0f mm (%+.0f mm)" + +msgid "\nCurves read from the design kept in the scenery: %d" +msgstr "\nŁuki odczytane z projektu zapisanego w scenerii: %d" + +msgid "\nCurves recognized from the paths, the scenery keeps no design for them" +msgstr "\nŁuki rozpoznane z odcinków, sceneria nie ma dla nich zapisanego projektu" + +msgid "Km of the start" +msgstr "Km początku" + +msgid "Chainage of the start of the fragment, kept with the design in the scenery; the vertical profile of these paths counts from it" +msgstr "Kilometraż początku fragmentu, zapisywany z projektem w scenerii; profil podłużny tych odcinków liczy się od niego" + +msgid "The design kept in %s for this fragment departs from its paths by up to %.2f m" +msgstr "Projekt zapisany w %s dla tego fragmentu odbiega od jego odcinków o maks. %.2f m" + +msgid "Take the kept design" +msgstr "Weź zapisany projekt" + +msgid "The kept design is loaded, applying it lays the paths by it again" +msgstr "Wczytano zapisany projekt, zastosowanie ułoży odcinki ponownie według niego" + +msgid "Loads the design kept in the scenery in place of the one read from the paths; nothing changes until it's applied" +msgstr "Wczytuje projekt zapisany w scenerii w miejsce odczytanego z odcinków; nic się nie zmienia do zastosowania" + +msgid "Forget the kept design" +msgstr "Zapomnij zapisany projekt" + +msgid "The kept design will be removed from " +msgstr "Zapisany projekt zostanie usunięty z " + +msgid "The paths stay as they are, the scenery no longer keeps a design for them" +msgstr "Odcinki zostają bez zmian, sceneria przestaje przechowywać dla nich projekt" diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index b735e344..aef7ac05 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -837,7 +837,7 @@ bool node_layers::pending() const namespace { // marks of the lines the editor keeps track of -std::array const editormarks{"//$p", "//$b", "//$c"}; +std::array const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c"}; } // namespace void node_layers::scan(layer_handle const Layer)