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)